diff --git a/.buildkite/linux/entrypoint b/.buildkite/linux/entrypoint index a519cfc3d..4c656aa5c 100755 --- a/.buildkite/linux/entrypoint +++ b/.buildkite/linux/entrypoint @@ -16,6 +16,7 @@ if [ "$CLEAR_RUST" = "1" ]; then rm -rf $BUILD_ROOT/rust fi +rm -f out/build.ninja ./ninja pylib qt check echo "--- Ensure libs importable" diff --git a/.idea.dist/repo.iml b/.idea.dist/repo.iml new file mode 100644 index 000000000..a9ec5ee1a --- /dev/null +++ b/.idea.dist/repo.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/.version b/.version index 6b856e54b..381fa89ef 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.08b5 +25.09.2 diff --git a/.vscode.dist/tasks.json b/.vscode.dist/tasks.json index 72eab9604..b89704d2e 100644 --- a/.vscode.dist/tasks.json +++ b/.vscode.dist/tasks.json @@ -12,8 +12,7 @@ "command": "tools/ninja.bat", "args": [ "pylib", - "qt", - "extract:win_amd64_audio" + "qt" ] } } diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f07..94f5c254e 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,2 @@ nodeLinker: node-modules +enableScripts: false diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 176689c93..2ec25da2d 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -49,6 +49,7 @@ Sander Santema Thomas Brownback Andrew Gaul kenden +Emil Hamrin Nickolay Yudin neitrinoweb Andreas Reis @@ -188,7 +189,7 @@ Christian Donat Asuka Minato Dillon Baldwin Voczi -Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> +Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> Themis Demetriades Luke Bartholomew Gregory Abrasaldo @@ -238,6 +239,22 @@ Bradley Szoke jcznk Thomas Rixen Siyuan Mattuwu Yan +Lee Doughty <32392044+leedoughty@users.noreply.github.com> +memchr +Max Romanowski +Aldlss +Hanna Nilsén +Elias Johansson Lara +Toby Penner +Danilo Spillebeen +Matbe766 +Amanda Sternberg +arold0 +nav1s +Ranjit Odedra +Eltaurus +jariji +Francisco Esteva ******************** diff --git a/Cargo.lock b/Cargo.lock index 3e7cf36b6..e9c74f6ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,12 +46,12 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.1.0" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ada2ee439075a3e70b6992fce18ac4e407cd05aea9ca3f75d2c0b0c20bbb364" +checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" dependencies = [ "cssparser", - "html5ever 0.31.0", + "html5ever 0.35.0", "maplit", "tendril", "url", @@ -94,7 +94,7 @@ dependencies = [ "axum", "axum-client-ip", "axum-extra", - "bitflags 2.9.1", + "bitflags 2.9.3", "blake3", "bytes", "chrono", @@ -114,7 +114,7 @@ dependencies = [ "futures", "hex", "htmlescape", - "hyper 1.6.0", + "hyper 1.7.0", "id_tree", "inflections", "itertools 0.14.0", @@ -133,7 +133,7 @@ dependencies = [ "rand 0.9.2", "rayon", "regex", - "reqwest 0.12.20", + "reqwest 0.12.23", "rusqlite", "rustls-pemfile 2.2.0", "scopeguard", @@ -145,7 +145,7 @@ dependencies = [ "sha1", "snafu", "strum 0.27.2", - "syn 2.0.103", + "syn 2.0.106", "tempfile", "tokio", "tokio-util", @@ -158,7 +158,7 @@ dependencies = [ "unicode-normalization", "windows 0.61.3", "wiremock", - "zip 4.1.0", + "zip 4.6.0", "zstd", ] @@ -183,6 +183,7 @@ dependencies = [ "itertools 0.14.0", "num-format", "phf 0.11.3", + "regex", "serde", "serde_json", "unic-langid", @@ -240,9 +241,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -270,35 +271,35 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -336,9 +337,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -348,16 +349,15 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.24" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d615619615a650c571269c00dca41db04b9210037fa76ed8239f70404ab56985" +checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" dependencies = [ + "compression-codecs", + "compression-core", "futures-core", - "memchr", "pin-project-lite", "tokio", - "zstd", - "zstd-safe", ] [[package]] @@ -379,18 +379,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -407,9 +407,9 @@ checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -419,13 +419,14 @@ checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", "axum-macros", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "itoa", "matchit", @@ -439,8 +440,10 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper 1.0.2", "tokio", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -488,7 +491,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "headers 0.4.1", + "headers", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -509,7 +512,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -578,9 +581,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" dependencies = [ "serde", ] @@ -620,15 +623,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "burn" @@ -656,7 +659,7 @@ dependencies = [ "burn-common", "burn-tensor", "derive-new 0.7.0", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "log", "num-traits", "portable-atomic", @@ -702,7 +705,7 @@ dependencies = [ "derive-new 0.7.0", "flate2", "half", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "log", "num-traits", "portable-atomic-util", @@ -729,7 +732,7 @@ dependencies = [ "derive-new 0.7.0", "futures-lite", "half", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "log", "num-traits", "rand 0.9.2", @@ -769,7 +772,7 @@ dependencies = [ "serde_json", "strum 0.27.2", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -781,7 +784,7 @@ dependencies = [ "derive-new 0.7.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -791,7 +794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d63629f2c8b82ee52dbb9c18becded5117c2faf57365dc271a55c16d139cd91a" dependencies = [ "burn-tensor", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "portable-atomic-util", "serde", ] @@ -846,7 +849,7 @@ dependencies = [ "burn-common", "burn-ir", "burn-tensor", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "log", "spin 0.10.0", ] @@ -863,7 +866,7 @@ dependencies = [ "cubecl", "derive-new 0.7.0", "half", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "num-traits", "rand 0.9.2", "rand_distr", @@ -905,22 +908,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -943,9 +946,9 @@ checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" [[package]] name = "camino" -version = "1.1.10" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" [[package]] name = "candle-core" @@ -977,9 +980,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -988,9 +991,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -1039,9 +1042,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -1049,9 +1052,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -1062,23 +1065,23 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1154,6 +1157,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "compression-codecs" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" +dependencies = [ + "compression-core", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1235,9 +1255,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1311,9 +1331,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -1345,7 +1365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1416,7 +1436,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03bf4211cdbd68bb0fb8291e0ed825c13da0d1ac01b7c02dce3cee44a6138be" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytemuck", "cubecl-common", "cubecl-ir", @@ -1487,9 +1507,9 @@ dependencies = [ [[package]] name = "cubecl-hip-sys" -version = "6.4.4348200" +version = "6.4.4348201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283fa7401056c53fb27e18f5d1806246bb5f937c4ecbd2453896f7a9ec495c73" +checksum = "678a20e5e38ce9c772bdd53596f2801ef210ae735ec2d7d46b5d5b675c09d929" dependencies = [ "libc", "regex", @@ -1543,7 +1563,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1555,7 +1575,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1657,7 +1677,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1668,7 +1688,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1679,11 +1699,10 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deadpool" -version = "0.10.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" dependencies = [ - "async-trait", "deadpool-runtime", "num_cpus", "tokio", @@ -1697,9 +1716,9 @@ checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "75d7cc94194b4dd0fa12845ef8c911101b7f37633cda14997a6e82099aa0b693" dependencies = [ "powerfmt", ] @@ -1712,7 +1731,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1723,18 +1742,18 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1755,7 +1774,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1765,7 +1784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1785,7 +1804,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", "unicode-xid", ] @@ -1844,7 +1863,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", + "redox_users 0.5.2", "windows-sys 0.60.2", ] @@ -1856,7 +1875,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -1928,20 +1947,20 @@ dependencies = [ [[package]] name = "embassy-futures" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" [[package]] name = "embed-resource" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0963f530273dc3022ab2bdc3fcd6d488e850256f2284a82b7413cb9481ee85dd" +checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.23", + "toml 0.9.5", "vswhom", "winreg 0.55.0", ] @@ -1964,7 +1983,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -2013,19 +2032,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -2062,14 +2081,14 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2137,7 +2156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2179,7 +2198,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -2196,9 +2215,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2307,9 +2326,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -2326,7 +2345,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -2614,9 +2633,9 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width 0.2.1", ] @@ -2644,7 +2663,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", "wasm-bindgen", ] @@ -2667,9 +2686,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -2680,8 +2699,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2711,7 +2730,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "gpu-alloc-types", ] @@ -2721,7 +2740,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -2742,9 +2761,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "gpu-descriptor-types", - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -2753,14 +2772,14 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -2777,9 +2796,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -2822,7 +2841,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2847,9 +2866,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -2863,22 +2882,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", -] - -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64 0.21.7", - "bytes", - "headers-core 0.2.0", - "http 0.2.12", - "httpdate", - "mime", - "sha1", + "hashbrown 0.15.5", ] [[package]] @@ -2889,22 +2893,13 @@ checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ "base64 0.22.1", "bytes", - "headers-core 0.3.0", + "headers-core", "http 1.3.1", "httpdate", "mime", "sha1", ] -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.12", -] - [[package]] name = "headers-core" version = "0.3.0" @@ -2963,13 +2958,12 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.31.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", - "markup5ever 0.16.1", + "markup5ever 0.35.0", "match_token", ] @@ -3035,6 +3029,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -3057,14 +3057,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3073,20 +3073,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.10", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3099,7 +3101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "rustls", "rustls-native-certs", @@ -3131,7 +3133,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -3141,9 +3143,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64 0.22.1", "bytes", @@ -3152,12 +3154,12 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -3290,9 +3292,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3319,7 +3321,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -3327,12 +3329,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -3353,7 +3355,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "inotify-sys", "libc", ] @@ -3386,6 +3388,17 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3462,7 +3475,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -3473,9 +3486,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -3542,6 +3555,7 @@ dependencies = [ name = "launcher" version = "1.0.0" dependencies = [ + "anki_i18n", "anki_io", "anki_process", "anyhow", @@ -3550,6 +3564,7 @@ dependencies = [ "embed-resource", "libc", "libc-stdhandle", + "locale_config", "serde_json", "widestring", "windows 0.61.3", @@ -3563,9 +3578,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.173" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libc-stdhandle" @@ -3584,7 +3599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -3595,11 +3610,11 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", "redox_syscall", ] @@ -3657,7 +3672,7 @@ dependencies = [ "itertools 0.14.0", "linkcheck", "regex", - "reqwest 0.12.20", + "reqwest 0.12.23", "strum 0.27.2", "tokio", ] @@ -3685,9 +3700,22 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] [[package]] name = "lock_api" @@ -3730,9 +3758,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "macerator" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce07f822458c4c303081d133a90610406162e7c8df17434956ac1892faf447b" +checksum = "8ac9c19702c37bae1a53d130a326b1c4f58cb17d472538cf547d44b46dbbe3aa" dependencies = [ "bytemuck", "cfg_aliases", @@ -3741,18 +3769,19 @@ dependencies = [ "moddef", "num-traits", "paste", + "rustc_version", ] [[package]] name = "macerator-macros" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b955a106dca78c0577269d67a6d56114abb8644b810fc995a22348276bb9dd" +checksum = "8cd48b535b9b37a25a2589ab8d4f997886a2c68f59960ce06588525f38dd4944" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -3786,9 +3815,9 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.16.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a8096766c229e8c88a3900c9b44b7e06aa7f7343cc229158c3e58ef8f9973a" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", "tendril", @@ -3809,22 +3838,22 @@ dependencies = [ [[package]] name = "match_token" -version = "0.1.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3860,12 +3889,13 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "mdbook" -version = "0.4.51" +version = "0.4.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87e65420ab45ca9c1b8cdf698f95b710cc826d373fa550f0f7fad82beac9328" +checksum = "93c284d2855916af7c5919cf9ad897cfc77d3c2db6f55429c7cfb769182030ec" dependencies = [ "ammonia", "anyhow", + "axum", "chrono", "clap", "clap_complete", @@ -3891,8 +3921,8 @@ dependencies = [ "tokio", "toml 0.5.11", "topological-sort", + "tower-http", "walkdir", - "warp", ] [[package]] @@ -3903,9 +3933,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", "stable_deref_trait", @@ -3926,7 +3956,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -3993,9 +4023,9 @@ dependencies = [ [[package]] name = "moddef" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e519fd9c6131c1c9a4a67f8bdc4f32eb4105b16c1468adea1b8e68c98c85ec4" +checksum = "4a0b3262dc837d2513fe2ef31ff8461352ef932dcca31ba0c0abe33547cf6b9b" [[package]] name = "multer" @@ -4028,11 +4058,11 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg_aliases", "codespan-reporting 0.12.0", "half", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "hexf-parse", "indexmap", "log", @@ -4041,7 +4071,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "thiserror 2.0.12", + "thiserror 2.0.16", "unicode-ident", ] @@ -4106,7 +4136,7 @@ dependencies = [ "maplit", "num_cpus", "regex", - "reqwest 0.12.20", + "reqwest 0.12.23", "serde_json", "sha2", "walkdir", @@ -4143,12 +4173,11 @@ dependencies = [ [[package]] name = "notify" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.1", - "filetime", + "bitflags 2.9.3", "fsevent-sys", "inotify", "kqueue", @@ -4157,7 +4186,7 @@ dependencies = [ "mio", "notify-types", "walkdir", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4189,12 +4218,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4315,23 +4343,24 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4340,7 +4369,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libloading", "nvml-wrapper-sys", "static_assertions", @@ -4366,6 +4395,26 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.36.7" @@ -4410,7 +4459,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4427,7 +4476,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4472,12 +4521,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking" version = "2.2.1" @@ -4544,9 +4587,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "percent-encoding-iri" @@ -4560,7 +4603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] @@ -4584,7 +4627,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4676,7 +4719,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4714,7 +4757,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4783,9 +4826,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -4819,12 +4862,12 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4855,18 +4898,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" [[package]] name = "prost" @@ -4894,7 +4937,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.103", + "syn 2.0.106", "tempfile", ] @@ -4908,7 +4951,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -4949,7 +4992,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "memchr", "pulldown-cmark-escape 0.10.1", "unicase", @@ -4961,7 +5004,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "getopts", "memchr", "pulldown-cmark-escape 0.11.0", @@ -5052,7 +5095,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -5065,14 +5108,14 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -5081,8 +5124,8 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2", - "thiserror 2.0.12", + "socket2 0.6.0", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -5090,9 +5133,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -5103,7 +5146,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -5111,16 +5154,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.0", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5134,9 +5177,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -5228,7 +5271,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -5245,9 +5288,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -5255,9 +5298,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -5271,11 +5314,11 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -5291,58 +5334,43 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "relative-path" @@ -5367,7 +5395,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -5398,9 +5426,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -5410,7 +5438,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -5513,7 +5541,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.103", + "syn 2.0.106", "unicode-ident", ] @@ -5528,14 +5556,14 @@ dependencies = [ "clap", "flate2", "junction", - "reqwest 0.12.20", + "reqwest 0.12.23", "sha2", "tar", "termcolor", "tokio", "which", "xz2", - "zip 4.1.0", + "zip 4.6.0", "zstd", ] @@ -5545,7 +5573,7 @@ version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5555,9 +5583,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -5582,22 +5610,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "once_cell", "ring", @@ -5616,7 +5644,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.3.0", ] [[package]] @@ -5649,9 +5677,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -5660,9 +5688,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -5717,12 +5745,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[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" @@ -5735,7 +5757,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5744,11 +5766,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5831,14 +5853,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -5864,23 +5886,23 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] [[package]] name = "serde_tuple" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9b739e59a0e07b7a73bc11c3dcd6abf790d0f54042b67a422d4bd1f6cf6c0" +checksum = "52569c5296679bd28e2457f067f97d270077df67da0340647da5412c8eac8d9e" dependencies = [ "serde", "serde_tuple_macros", @@ -5888,13 +5910,13 @@ dependencies = [ [[package]] name = "serde_tuple_macros" -version = "1.0.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87546e85c5047d03b454d12ee25266fc269a461a4029956ca58d246b9aefae" +checksum = "2f46c707781471741d5f2670edb36476479b26e94cf43efe21ca3c220b97ef2e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.106", ] [[package]] @@ -5948,9 +5970,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -5996,23 +6018,23 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snafu" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" +checksum = "4800ae0e2ebdfaea32ffb9745642acdc378740dcbd74d3fb3cd87572a34810c6" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" +checksum = "186f5ba9999528053fb497fdf0dd330efcc69cfe4ad03776c9d704bc54fee10f" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -6031,6 +6053,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -6057,7 +6089,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -6118,7 +6150,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.27.1", + "strum_macros 0.27.2", ] [[package]] @@ -6131,20 +6163,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -6166,9 +6197,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -6198,7 +6229,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -6207,7 +6238,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "byteorder", "enum-as-inner", "libc", @@ -6221,7 +6252,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "byteorder", "enum-as-inner", "libc", @@ -6240,7 +6271,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.56.0", + "windows 0.57.0", ] [[package]] @@ -6266,9 +6297,9 @@ dependencies = [ [[package]] name = "systemstat" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668a4db78b439df482c238f559e4ea869017f9e62ef0a059c8bfcd841a4df544" +checksum = "5021f5184d44b26fb184acd689671bbe1e4bbd24bbdaa6bc7ec383fad32d2033" dependencies = [ "bytesize", "lazy_static", @@ -6297,15 +6328,15 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6330,12 +6361,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6360,11 +6391,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -6375,18 +6406,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -6409,12 +6440,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", @@ -6424,15 +6454,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "a9108bb380861b07264b950ded55a44a14a4adc68b9f5efd85aafc3aa4d40a68" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "7182799245a7264ce590b349d90338f1c1affad93d2639aed5f8f69c090b334c" dependencies = [ "num-conv", "time-core", @@ -6460,9 +6490,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -6475,19 +6505,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6498,7 +6530,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -6523,9 +6555,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", @@ -6535,9 +6567,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -6557,14 +6589,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ + "indexmap", "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] @@ -6572,6 +6607,12 @@ name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] @@ -6583,18 +6624,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_parser" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "topological-sort" @@ -6624,13 +6671,22 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytes", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", "iri-string", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -6675,13 +6731,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -6707,14 +6763,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -6731,20 +6787,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ - "byteorder", "bytes", "data-encoding", "http 1.3.1", "httparse", "log", - "rand 0.8.5", + "rand 0.9.2", "sha1", - "thiserror 1.0.69", - "url", + "thiserror 2.0.16", "utf-8", ] @@ -6850,7 +6904,7 @@ checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" dependencies = [ "proc-macro-hack", "quote", - "syn 2.0.103", + "syn 2.0.106", "unic-langid-impl", ] @@ -6943,9 +6997,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -6973,9 +7027,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" [[package]] name = "valuable" @@ -6991,7 +7045,7 @@ checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7045,34 +7099,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "headers 0.3.9", - "http 0.2.12", - "hyper 0.14.32", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-tungstenite", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -7081,11 +7107,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -7119,7 +7145,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -7154,7 +7180,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7215,9 +7241,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -7229,10 +7255,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg_aliases", "document-features", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "js-sys", "log", "naga", @@ -7259,10 +7285,10 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg_aliases", "document-features", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "indexmap", "log", "naga", @@ -7273,7 +7299,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.16", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", @@ -7318,7 +7344,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.9.1", + "bitflags 2.9.3", "block", "bytemuck", "cfg-if", @@ -7329,7 +7355,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "js-sys", "khronos-egl", "libc", @@ -7347,7 +7373,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.16", "wasm-bindgen", "web-sys", "wgpu-types", @@ -7361,11 +7387,11 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytemuck", "js-sys", "log", - "thiserror 2.0.12", + "thiserror 2.0.16", "web-sys", ] @@ -7404,11 +7430,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7419,11 +7445,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core 0.56.0", + "windows-core 0.57.0", "windows-targets 0.52.6", ] @@ -7461,12 +7487,12 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-implement 0.56.0", - "windows-interface 0.56.0", + "windows-implement 0.57.0", + "windows-interface 0.57.0", "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -7510,13 +7536,13 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7527,7 +7553,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7538,18 +7564,18 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7560,7 +7586,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7571,7 +7597,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7669,7 +7695,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -7705,10 +7731,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -7868,9 +7895,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -7903,18 +7930,17 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wiremock" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101681b74cd87b5899e87bcf5a64e83334dd313fcd3053ea72e6dba18928e301" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "async-trait", "base64 0.22.1", "deadpool", "futures", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "log", "once_cell", @@ -7926,13 +7952,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] name = "wrapcenum-derive" @@ -7943,7 +7966,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -7954,9 +7977,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "xattr" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", "rustix", @@ -7964,9 +7987,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xml5ever" @@ -8020,7 +8043,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", "synstructure", ] @@ -8032,28 +8055,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -8073,7 +8096,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", "synstructure", ] @@ -8096,9 +8119,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke 0.8.0", "zerofrom", @@ -8113,7 +8136,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.106", ] [[package]] @@ -8133,9 +8156,9 @@ dependencies = [ [[package]] name = "zip" -version = "4.1.0" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0" +checksum = "c034aa6c54f654df20e7dc3713bc51705c12f280748fb6d7f40f87c696623e34" dependencies = [ "arbitrary", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 27d14ce8c..fe7f5acd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ ninja_gen = { "path" = "build/ninja_gen" } unicase = "=2.6.0" # any changes could invalidate sqlite indexes # normal -ammonia = "4.1.0" +ammonia = "4.1.2" anyhow = "1.0.98" async-compression = { version = "0.4.24", features = ["zstd", "tokio"] } async-stream = "0.3.6" @@ -92,6 +92,7 @@ itertools = "0.14.0" junction = "1.2.0" libc = "0.2" libc-stdhandle = "0.1" +locale_config = "0.3.0" maplit = "1.0.2" nom = "8.0.0" num-format = "0.4.4" @@ -133,7 +134,7 @@ tokio-util = { version = "0.7.15", features = ["io"] } tower-http = { version = "0.6.6", features = ["trace"] } tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] } tracing-appender = "0.2.3" -tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"] } +tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] } unic-langid = { version = "0.9.6", features = ["macros"] } unic-ucd-category = "0.9.0" unicode-normalization = "0.1.24" diff --git a/build/ninja_gen/src/action.rs b/build/ninja_gen/src/action.rs index 97d77de95..14f08deb5 100644 --- a/build/ninja_gen/src/action.rs +++ b/build/ninja_gen/src/action.rs @@ -49,6 +49,46 @@ pub trait BuildAction { } fn name(&self) -> &'static str { - std::any::type_name::().split("::").last().unwrap() + std::any::type_name::() + .split("::") + .last() + .unwrap() + .split('<') + .next() + .unwrap() } } + +#[cfg(test)] +trait TestBuildAction {} + +#[cfg(test)] +impl BuildAction for T { + fn command(&self) -> &str { + "test" + } + fn files(&mut self, _build: &mut impl FilesHandle) {} +} + +#[allow(dead_code, unused_variables)] +#[test] +fn should_strip_regions_in_type_name() { + struct Bare; + impl TestBuildAction for Bare {} + assert_eq!(Bare {}.name(), "Bare"); + + struct WithLifeTime<'a>(&'a str); + impl TestBuildAction for WithLifeTime<'_> {} + assert_eq!(WithLifeTime("test").name(), "WithLifeTime"); + + struct WithMultiLifeTime<'a, 'b>(&'a str, &'b str); + impl TestBuildAction for WithMultiLifeTime<'_, '_> {} + assert_eq!( + WithMultiLifeTime("test", "test").name(), + "WithMultiLifeTime" + ); + + struct WithGeneric(T); + impl TestBuildAction for WithGeneric {} + assert_eq!(WithGeneric(3).name(), "WithGeneric"); +} diff --git a/build/ninja_gen/src/archives.rs b/build/ninja_gen/src/archives.rs index 3f87d3ff5..3d2120b06 100644 --- a/build/ninja_gen/src/archives.rs +++ b/build/ninja_gen/src/archives.rs @@ -67,7 +67,7 @@ impl Platform { } /// Append .exe to path if on Windows. -pub fn with_exe(path: &str) -> Cow { +pub fn with_exe(path: &str) -> Cow<'_, str> { if cfg!(windows) { format!("{path}.exe").into() } else { diff --git a/build/ninja_gen/src/node.rs b/build/ninja_gen/src/node.rs index b7b66225b..38baa8f62 100644 --- a/build/ninja_gen/src/node.rs +++ b/build/ninja_gen/src/node.rs @@ -98,7 +98,7 @@ impl BuildAction for YarnInstall<'_> { } } -fn with_cmd_ext(bin: &str) -> Cow { +fn with_cmd_ext(bin: &str) -> Cow<'_, str> { if cfg!(windows) { format!("{bin}.cmd").into() } else { diff --git a/build/runner/src/yarn.rs b/build/runner/src/yarn.rs index 9e1bd5b58..7724ed04a 100644 --- a/build/runner/src/yarn.rs +++ b/build/runner/src/yarn.rs @@ -28,7 +28,11 @@ pub fn setup_yarn(args: YarnArgs) { .arg("--ignore-scripts"), ); } else { - run_command(Command::new(&args.yarn_bin).arg("install")); + run_command( + Command::new(&args.yarn_bin) + .arg("install") + .arg("--immutable"), + ); } std::fs::write(args.stamp, b"").unwrap(); diff --git a/cargo/licenses.json b/cargo/licenses.json index 4eeaeed51..53b832fda 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -599,6 +599,22 @@ "name": "colored", "repository": "https://github.com/mackwic/colored" }, + { + "authors": "Wim Looman |Allen Bui ", + "description": "Adaptors for various compression algorithms.", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "compression-codecs", + "repository": "https://github.com/Nullus157/async-compression" + }, + { + "authors": "Wim Looman |Allen Bui ", + "description": "Abstractions for compression algorithms.", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "compression-core", + "repository": "https://github.com/Nullus157/async-compression" + }, { "authors": "Stjepan Glavina |Taiki Endo |John Nunley ", "description": "Concurrent multi-producer multi-consumer queue", @@ -1759,6 +1775,14 @@ "name": "http-body-util", "repository": "https://github.com/hyperium/http-body" }, + { + "authors": null, + "description": "No-dep range header parser", + "license": "MIT", + "license_file": null, + "name": "http-range-header", + "repository": "https://github.com/MarcusGrass/parse-range-headers" + }, { "authors": "Sean McArthur ", "description": "A tiny, safe, speedy, zero-copy HTTP/1.x parser.", @@ -1943,6 +1967,14 @@ "name": "intl_pluralrules", "repository": "https://github.com/zbraniecki/pluralrules" }, + { + "authors": "quininer ", + "description": "The low-level `io_uring` userspace interface for Rust", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "io-uring", + "repository": "https://github.com/tokio-rs/io-uring" + }, { "authors": "Kris Price ", "description": "Provides types and useful methods for working with IPv4 and IPv6 network addresses, commonly called IP prefixes. The new `IpNet`, `Ipv4Net`, and `Ipv6Net` types build on the existing `IpAddr`, `Ipv4Addr`, and `Ipv6Addr` types already provided in Rust's standard library and align to their design to stay consistent. The module also provides useful traits that extend `Ipv4Addr` and `Ipv6Addr` with methods for `Add`, `Sub`, `BitAnd`, and `BitOr` operations. The module only uses stable feature so it is guaranteed to compile using the stable toolchain.", @@ -2168,7 +2200,7 @@ "repository": "https://github.com/servo/html5ever" }, { - "authors": null, + "authors": "The html5ever Project Developers", "description": "Procedural macro for html5ever.", "license": "Apache-2.0 OR MIT", "license_file": null, @@ -2194,7 +2226,7 @@ { "authors": "Ibraheem Ahmed ", "description": "A high performance, zero-copy URL router.", - "license": "MIT AND BSD-3-Clause", + "license": "BSD-3-Clause AND MIT", "license_file": null, "name": "matchit", "repository": "https://github.com/ibraheemdev/matchit" @@ -2567,14 +2599,6 @@ "name": "ordered-float", "repository": "https://github.com/reem/rust-ordered-float" }, - { - "authors": "Daniel Salvadori ", - "description": "Provides a macro to simplify operator overloading.", - "license": "MIT", - "license_file": null, - "name": "overload", - "repository": "https://github.com/danaugrs/overload" - }, { "authors": "Stjepan Glavina |The Rust Project Developers", "description": "Thread parking and unparking", @@ -3040,7 +3064,7 @@ "repository": "https://github.com/bluss/rawpointer/" }, { - "authors": "Niko Matsakis |Josh Stone ", + "authors": null, "description": "Simple work-stealing parallelism for Rust", "license": "Apache-2.0 OR MIT", "license_file": null, @@ -3048,7 +3072,7 @@ "repository": "https://github.com/rayon-rs/rayon" }, { - "authors": "Niko Matsakis |Josh Stone ", + "authors": null, "description": "Core APIs for Rayon", "license": "Apache-2.0 OR MIT", "license_file": null, @@ -3095,28 +3119,12 @@ "name": "regex", "repository": "https://github.com/rust-lang/regex" }, - { - "authors": "Andrew Gallant ", - "description": "Automata construction and matching using regular expressions.", - "license": "MIT OR Unlicense", - "license_file": null, - "name": "regex-automata", - "repository": "https://github.com/BurntSushi/regex-automata" - }, { "authors": "The Rust Project Developers|Andrew Gallant ", "description": "Automata construction and matching using regular expressions.", "license": "Apache-2.0 OR MIT", "license_file": null, "name": "regex-automata", - "repository": "https://github.com/rust-lang/regex/tree/master/regex-automata" - }, - { - "authors": "The Rust Project Developers", - "description": "A regular expression parser.", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "name": "regex-syntax", "repository": "https://github.com/rust-lang/regex" }, { @@ -3125,7 +3133,7 @@ "license": "Apache-2.0 OR MIT", "license_file": null, "name": "regex-syntax", - "repository": "https://github.com/rust-lang/regex/tree/master/regex-syntax" + "repository": "https://github.com/rust-lang/regex" }, { "authors": "John-John Tedro ", @@ -3455,14 +3463,6 @@ "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", @@ -3711,14 +3711,6 @@ "name": "syn", "repository": "https://github.com/dtolnay/syn" }, - { - "authors": "David Tolnay ", - "description": "Parser for Rust source code", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "name": "syn", - "repository": "https://github.com/dtolnay/syn" - }, { "authors": "Actyx AG ", "description": "A tool for enlisting the compiler's help in proving the absence of concurrency", @@ -3927,6 +3919,14 @@ "name": "tokio-rustls", "repository": "https://github.com/rustls/tokio-rustls" }, + { + "authors": "Daniel Abramov |Alexey Galakhov ", + "description": "Tokio binding for Tungstenite, the Lightweight stream-based WebSocket implementation", + "license": "MIT", + "license_file": null, + "name": "tokio-tungstenite", + "repository": "https://github.com/snapview/tokio-tungstenite" + }, { "authors": "Tokio Contributors ", "description": "Additional utilities for working with Tokio.", @@ -3951,14 +3951,6 @@ "name": "toml_edit", "repository": "https://github.com/toml-rs/toml" }, - { - "authors": null, - "description": "A low-level interface for writing out TOML", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "name": "toml_write", - "repository": "https://github.com/toml-rs/toml" - }, { "authors": "Tower Maintainers ", "description": "Tower is a library of modular and reusable components for building robust clients and servers.", @@ -4047,6 +4039,14 @@ "name": "try-lock", "repository": "https://github.com/seanmonstar/try-lock" }, + { + "authors": "Alexey Galakhov|Daniel Abramov", + "description": "Lightweight stream-based WebSocket implementation", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "tungstenite", + "repository": "https://github.com/snapview/tungstenite-rs" + }, { "authors": "Jacob Brown ", "description": "Provides a typemap container with FxHashMap", @@ -4154,7 +4154,7 @@ { "authors": "David Tolnay ", "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", - "license": "(MIT OR Apache-2.0) AND Unicode-3.0", + "license": "(Apache-2.0 OR MIT) AND Unicode-3.0", "license_file": null, "name": "unicode-ident", "repository": "https://github.com/dtolnay/unicode-ident" @@ -4920,11 +4920,11 @@ "repository": "https://github.com/LukeMathWalker/wiremock-rs" }, { - "authors": null, - "description": "Runtime support for the `wit-bindgen` crate", + "authors": "Alex Crichton ", + "description": "Rust bindings generator and runtime support for WIT and the component model. Used when compiling Rust programs to the component model.", "license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", "license_file": null, - "name": "wit-bindgen-rt", + "name": "wit-bindgen", "repository": "https://github.com/bytecodealliance/wit-bindgen" }, { diff --git a/docs/docker/Dockerfile b/docs/docker/Dockerfile index 6682f70f6..381d27d1c 100644 --- a/docs/docker/Dockerfile +++ b/docs/docker/Dockerfile @@ -1,35 +1,78 @@ -# This Dockerfile uses three stages. -# 1. Compile anki (and dependencies) and build python wheels. -# 2. Create a virtual environment containing anki and its dependencies. -# 3. Create a final image that only includes anki's virtual environment and required -# system packages. +# This is a user-contributed Dockerfile. No official support is available. -ARG PYTHON_VERSION="3.9" ARG DEBIAN_FRONTEND="noninteractive" -# Build anki. -FROM python:$PYTHON_VERSION AS build -RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64 \ - > /usr/local/bin/bazel \ - && chmod +x /usr/local/bin/bazel \ - # Bazel expects /usr/bin/python - && ln -s /usr/local/bin/python /usr/bin/python +FROM ubuntu:24.04 AS build WORKDIR /opt/anki -COPY . . -# Build python wheels. +ENV PYTHON_VERSION="3.13" + + +# System deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + git \ + build-essential \ + pkg-config \ + libssl-dev \ + libbz2-dev \ + libreadline-dev \ + libsqlite3-dev \ + libffi-dev \ + zlib1g-dev \ + liblzma-dev \ + ca-certificates \ + ninja-build \ + rsync \ + libglib2.0-0 \ + libgl1 \ + libx11-6 \ + libxext6 \ + libxrender1 \ + libxkbcommon0 \ + libxkbcommon-x11-0 \ + libxcb1 \ + libxcb-render0 \ + libxcb-shm0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-shape0 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxcb-xinput0 \ + libsm6 \ + libice6 \ + && rm -rf /var/lib/apt/lists/* + +# install rust with rustup +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Install uv and Python 3.13 with uv +RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ + && ln -s /root/.local/bin/uv /usr/local/bin/uv +ENV PATH="/root/.local/bin:${PATH}" + +RUN uv python install ${PYTHON_VERSION} --default + +COPY . . + RUN ./tools/build + # Install pre-compiled Anki. -FROM python:${PYTHON_VERSION}-slim as installer +FROM python:3.13-slim AS installer WORKDIR /opt/anki/ -COPY --from=build /opt/anki/wheels/ wheels/ +COPY --from=build /opt/anki/out/wheels/ wheels/ # Use virtual environment. RUN python -m venv venv \ && ./venv/bin/python -m pip install --no-cache-dir setuptools wheel \ && ./venv/bin/python -m pip install --no-cache-dir /opt/anki/wheels/*.whl + # We use another build stage here so we don't include the wheels in the final image. -FROM python:${PYTHON_VERSION}-slim as final +FROM python:3.13-slim AS final COPY --from=installer /opt/anki/venv /opt/anki/venv ENV PATH=/opt/anki/venv/bin:$PATH # Install run-time dependencies. @@ -59,9 +102,9 @@ RUN apt-get update \ libxrender1 \ libxtst6 \ && rm -rf /var/lib/apt/lists/* + # Add non-root user. RUN useradd --create-home anki USER anki WORKDIR /work -ENTRYPOINT ["/opt/anki/venv/bin/anki"] -LABEL maintainer="Jakub Kaczmarzyk " +ENTRYPOINT ["/opt/anki/venv/bin/anki"] \ No newline at end of file diff --git a/docs/editing.md b/docs/editing.md index ba3fd6fce..42a92c5a8 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -46,10 +46,14 @@ see and install a number of recommended extensions. ## PyCharm/IntelliJ -If you decide to use PyCharm instead of VS Code, there are somethings to be -aware of. +### Setting up Python environment -### Pylib References +To make PyCharm recognize `anki` and `aqt` imports, you need to add source paths to _Settings > Project Structure_. +You can copy the provided .idea.dist directory to set up the paths automatically: -You'll need to use File>Project Structure to tell IntelliJ that pylib/ is a -sources root, so it knows references to 'anki' in aqt are valid. +``` +mkdir .idea && cd .idea +ln -sf ../.idea.dist/* . +``` + +You also need to add a new Python interpreter under _Settings > Python > Interpreter_ pointing to the Python executable under `out/pyenv` (available after building Anki). diff --git a/ftl/core-repo b/ftl/core-repo index a599715d3..ec5e4cad6 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit a599715d3c27ff2eb895c749f3534ab73d83dad1 +Subproject commit ec5e4cad6242e538cacf52265243668f0de5da80 diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index acaa9802d..f4aca0ec1 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -382,10 +382,8 @@ deck-config-which-deck = Which deck would you like to display options for? ## Messages related to the FSRS scheduler deck-config-updating-cards = Updating cards: { $current_cards_count }/{ $total_cards_count }... -deck-config-invalid-parameters = The provided FSRS parameters are invalid. Leave them blank to use the default parameters. +deck-config-invalid-parameters = The provided FSRS parameters are invalid. Leave them blank to use the default values. deck-config-not-enough-history = Insufficient review history to perform this operation. -deck-config-unable-to-determine-desired-retention = - Unable to determine a minimum recommended retention. deck-config-must-have-400-reviews = { $count -> [one] Only { $count } review was found. @@ -394,7 +392,6 @@ deck-config-must-have-400-reviews = # Numbers that control how aggressively the FSRS algorithm schedules cards deck-config-weights = FSRS parameters deck-config-compute-optimal-weights = Optimize FSRS parameters -deck-config-compute-minimum-recommended-retention = Minimum recommended retention deck-config-optimize-button = Optimize Current Preset # Indicates that a given function or label, provided via the "text" variable, operates slowly. deck-config-slow-suffix = { $text } (slow) @@ -407,7 +404,6 @@ deck-config-historical-retention = Historical retention deck-config-smaller-is-better = Smaller numbers indicate a better fit to your review history. deck-config-steps-too-large-for-fsrs = When FSRS is enabled, steps of 1 day or more are not recommended. deck-config-get-params = Get Params -deck-config-predicted-minimum-recommended-retention = Minimum recommended retention: { $num } deck-config-complete = { $num }% complete. deck-config-iterations = Iteration: { $count }... deck-config-reschedule-cards-on-change = Reschedule cards on change @@ -468,12 +464,7 @@ deck-config-compute-optimal-weights-tooltip2 = By default, parameters will be calculated from the review history of all decks using the current preset. You can optionally adjust the search before calculating the parameters, if you'd like to alter which cards are used for optimizing the parameters. -deck-config-compute-optimal-retention-tooltip4 = - This tool will attempt to find the desired retention value - that will lead to the most material learnt, in the least amount of time. The calculated number can serve as a reference - when deciding what to set your desired retention to. You may wish to choose a higher desired retention if you’re - willing to invest more study time to achieve it. Setting your desired retention lower than the minimum - is not recommended, as it will lead to a higher workload, because of the high forgetting rate. + deck-config-please-save-your-changes-first = Please save your changes first. deck-config-workload-factor-change = Approximate workload: {$factor}x (compared to {$previousDR}% desired retention) @@ -507,6 +498,7 @@ deck-config-desired-retention-below-optimal = Your desired retention is below op # cards that can be recalled or retrieved on a specific date. deck-config-fsrs-simulator-experimental = FSRS Simulator (Experimental) deck-config-fsrs-simulate-desired-retention-experimental = FSRS Desired Retention Simulator (Experimental) +deck-config-fsrs-simulate-save-preset = After optimizing, please save your deck preset before running the simulator. deck-config-fsrs-desired-retention-help-me-decide-experimental = Help Me Decide (Experimental) deck-config-additional-new-cards-to-simulate = Additional new cards to simulate deck-config-simulate = Simulate @@ -546,6 +538,16 @@ deck-config-fsrs-good-fit = Health Check: deck-config-fsrs-simulator-radio-ratio = Time / Memorized Ratio # $time here is pre-formatted e.g. "10 Seconds" deck-config-fsrs-simulator-ratio-tooltip = { $time } per memorized card +deck-config-unable-to-determine-desired-retention = + Unable to determine a minimum recommended retention. +deck-config-predicted-minimum-recommended-retention = Minimum recommended retention: { $num } +deck-config-compute-minimum-recommended-retention = Minimum recommended retention +deck-config-compute-optimal-retention-tooltip4 = + This tool will attempt to find the desired retention value + that will lead to the most material learnt, in the least amount of time. The calculated number can serve as a reference + when deciding what to set your desired retention to. You may wish to choose a higher desired retention if you’re + willing to invest more study time to achieve it. Setting your desired retention lower than the minimum + is not recommended, as it will lead to a higher workload, because of the high forgetting rate. deck-config-plotted-on-x-axis = (Plotted on the X-axis) deck-config-a-100-day-interval = { $days -> diff --git a/ftl/core/launcher.ftl b/ftl/core/launcher.ftl new file mode 100644 index 000000000..ee3aa6320 --- /dev/null +++ b/ftl/core/launcher.ftl @@ -0,0 +1,38 @@ +launcher-title = Anki Launcher +launcher-press-enter-to-install = Press the Enter/Return key on your keyboard to install or update Anki. +launcher-press-enter-to-start = Press enter to start Anki. +launcher-anki-will-start-shortly = Anki will start shortly. +launcher-you-can-close-this-window = You can close this window. +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-revert-to-previous = Revert to previous version ({ $prev }) +launcher-allow-betas = Allow betas: { $state } +launcher-on = on +launcher-off = off +launcher-cache-downloads = Cache downloads: { $state } +launcher-download-mirror = Download mirror: { $state } +launcher-uninstall = Uninstall Anki +launcher-invalid-input = Invalid input. Please try again. +launcher-latest-releases = Latest releases: { $releases } +launcher-enter-the-version-you-want = Enter the version you want to install: +launcher-versions-before-cant-be-installed = Versions before 2.1.50 can't be installed. +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-cancelled = Uninstall cancelled. +launcher-program-files-removed = Program files removed. +launcher-remove-all-profiles-confirm = Remove all profiles/cards? (y/n) +launcher-user-data-removed = User data removed. +launcher-download-mirror-options = Download mirror options: +launcher-mirror-no-mirror = No mirror +launcher-mirror-china = China +launcher-mirror-disabled = Mirror disabled. +launcher-mirror-china-enabled = China mirror enabled. +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. diff --git a/ftl/core/studying.ftl b/ftl/core/studying.ftl index ed3f8eb30..a317a68ba 100644 --- a/ftl/core/studying.ftl +++ b/ftl/core/studying.ftl @@ -46,6 +46,20 @@ studying-type-answer-unknown-field = Type answer: unknown field { $val } studying-unbury = Unbury studying-what-would-you-like-to-unbury = What would you like to unbury? studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet. +studying-card-studied-in-minute = + { $cards -> + [one] { $cards } card + *[other] { $cards } cards + } studied in + { $minutes -> + [one] { $minutes } minute. + *[other] { $minutes } minutes. + } +studying-question-time-elapsed = Question time elapsed +studying-answer-time-elapsed = Answer time elapsed + +## OBSOLETE; you do not need to translate this + studying-card-studied-in = { $count -> [one] { $count } card studied in @@ -56,5 +70,3 @@ studying-minute = [one] { $count } minute. *[other] { $count } minutes. } -studying-question-time-elapsed = Question time elapsed -studying-answer-time-elapsed = Answer time elapsed diff --git a/ftl/qt-repo b/ftl/qt-repo index bb4207f3b..0b7c53023 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit bb4207f3b8e9a7c428db282d12c75b850be532f3 +Subproject commit 0b7c530233390d73b706f012bbe7489539925c7d diff --git a/package.json b/package.json index 9f12133db..f85945cd5 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "resolutions": { "canvas": "npm:empty-npm-package@1.0.0", "cookie": "0.7.0", + "devalue": "^5.3.2", "vite": "6" }, "browserslist": [ diff --git a/proto/anki/collection.proto b/proto/anki/collection.proto index de0ff08d6..330413613 100644 --- a/proto/anki/collection.proto +++ b/proto/anki/collection.proto @@ -20,6 +20,7 @@ service CollectionService { rpc LatestProgress(generic.Empty) returns (Progress); rpc SetWantsAbort(generic.Empty) returns (generic.Empty); rpc SetLoadBalancerEnabled(generic.Bool) returns (OpChanges); + rpc GetCustomColours(generic.Empty) returns (GetCustomColoursResponse); } // Implicitly includes any of the above methods that are not listed in the @@ -163,3 +164,7 @@ message CreateBackupRequest { bool force = 2; bool wait_for_completion = 3; } + +message GetCustomColoursResponse { + repeated string colours = 1; +} diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 95b929c5c..1d733a369 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -27,6 +27,9 @@ service FrontendService { rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty); // Warns python that the deck option web view is ready to receive requests. rpc deckOptionsReady(generic.Empty) returns (generic.Empty); + + // Save colour picker's custom colour palette + rpc SaveCustomColours(generic.Empty) returns (generic.Empty); } service BackendFrontendService {} diff --git a/proto/anki/notes.proto b/proto/anki/notes.proto index 39bbcf1e2..f147a599d 100644 --- a/proto/anki/notes.proto +++ b/proto/anki/notes.proto @@ -59,7 +59,7 @@ message AddNoteRequest { } message AddNoteResponse { - collection.OpChanges changes = 1; + collection.OpChangesWithCount changes = 1; int64 note_id = 2; } diff --git a/proto/anki/search.proto b/proto/anki/search.proto index bb417294c..e87a063c9 100644 --- a/proto/anki/search.proto +++ b/proto/anki/search.proto @@ -74,10 +74,15 @@ message SearchNode { repeated SearchNode nodes = 1; Joiner joiner = 2; } + enum FieldSearchMode { + FIELD_SEARCH_MODE_NORMAL = 0; + FIELD_SEARCH_MODE_REGEX = 1; + FIELD_SEARCH_MODE_NOCOMBINING = 2; + } message Field { string field_name = 1; string text = 2; - bool is_re = 3; + FieldSearchMode mode = 3; } oneof filter { diff --git a/proto/anki/stats.proto b/proto/anki/stats.proto index a5639f841..1bd0fe630 100644 --- a/proto/anki/stats.proto +++ b/proto/anki/stats.proto @@ -37,6 +37,8 @@ message CardStatsResponse { uint32 ease = 5; float taken_secs = 6; optional cards.FsrsMemoryState memory_state = 7; + // seconds + uint32 last_interval = 8; } repeated StatsRevlogEntry revlog = 1; int64 card_id = 2; diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index c64ffdb8b..60360470c 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -528,7 +528,7 @@ class Collection(DeprecatedNamesMixin): def new_note(self, notetype: NotetypeDict) -> Note: return Note(self, notetype) - def add_note(self, note: Note, deck_id: DeckId) -> OpChanges: + def add_note(self, note: Note, deck_id: DeckId) -> OpChangesWithCount: hooks.note_will_be_added(self, note, deck_id) out = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id) note.id = NoteId(out.note_id) diff --git a/pylib/anki/foreign_data/mnemosyne.py b/pylib/anki/foreign_data/mnemosyne.py index 9c35be38f..e2fc56148 100644 --- a/pylib/anki/foreign_data/mnemosyne.py +++ b/pylib/anki/foreign_data/mnemosyne.py @@ -175,8 +175,8 @@ class MnemoFact: def fact_view(self) -> type[MnemoFactView]: try: fact_view = self.cards[0].fact_view_id - except IndexError as err: - raise Exception(f"Fact {self.id} has no cards") from err + except IndexError: + return FrontOnly if fact_view.startswith("1.") or fact_view.startswith("1::"): return FrontOnly diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index b639b0416..1b0599a2f 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -18,7 +18,7 @@ from anki._legacy import DeprecatedNamesMixinForModule TR = anki._fluent.LegacyTranslationEnum FormatTimeSpan = _pb.FormatTimespanRequest - +# When adding new languages here, check lang_to_disk_lang() below langs = sorted( [ ("Afrikaans", "af_ZA"), @@ -38,6 +38,7 @@ langs = sorted( ("Italiano", "it_IT"), ("lo jbobau", "jbo_EN"), ("Lenga d'òc", "oc_FR"), + ("Қазақша", "kk_KZ"), ("Magyar", "hu_HU"), ("Nederlands", "nl_NL"), ("Norsk", "nb_NO"), @@ -64,6 +65,7 @@ langs = sorted( ("Українська мова", "uk_UA"), ("Հայերեն", "hy_AM"), ("עִבְרִית", "he_IL"), + ("ייִדיש", "yi"), ("العربية", "ar_SA"), ("فارسی", "fa_IR"), ("ภาษาไทย", "th_TH"), @@ -104,6 +106,7 @@ compatMap = { "it": "it_IT", "ja": "ja_JP", "jbo": "jbo_EN", + "kk": "kk_KZ", "ko": "ko_KR", "la": "la_LA", "mn": "mn_MN", @@ -126,6 +129,7 @@ compatMap = { "uk": "uk_UA", "uz": "uz_UZ", "vi": "vi_VN", + "yi": "yi", } @@ -233,7 +237,7 @@ def get_def_lang(user_lang: str | None = None) -> tuple[int, str]: def is_rtl(lang: str) -> bool: - return lang in ("he", "ar", "fa", "ug") + return lang in ("he", "ar", "fa", "ug", "yi") # strip off unicode isolation markers from a translated string diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py index 236096572..72e7fdb8a 100644 --- a/pylib/tests/test_find.py +++ b/pylib/tests/test_find.py @@ -32,6 +32,7 @@ def test_find_cards(): note = col.newNote() note["Front"] = "cat" note["Back"] = "sheep" + note.tags.append("conjunção größte") col.addNote(note) catCard = note.cards()[0] m = col.models.current() @@ -68,6 +69,8 @@ def test_find_cards(): col.tags.bulk_remove(col.db.list("select id from notes"), "foo") assert len(col.find_cards("tag:foo")) == 0 assert len(col.find_cards("tag:bar")) == 5 + assert len(col.find_cards("tag:conjuncao tag:groste")) == 0 + assert len(col.find_cards("tag:nc:conjuncao tag:nc:groste")) == 1 # text searches assert len(col.find_cards("cat")) == 2 assert len(col.find_cards("cat -dog")) == 1 diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 03e989f2c..95e034037 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -226,6 +226,7 @@ def show(mw: aqt.AnkiQt) -> QDialog: "Anon_0000", "Bilolbek Normuminov", "Sagiv Marzini", + "Zhanibek Rassululy", ) ) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 86e8a25b1..01d7423d8 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -8,7 +8,7 @@ from collections.abc import Callable import aqt.editor import aqt.forms from anki._legacy import deprecated -from anki.collection import OpChanges, SearchNode +from anki.collection import OpChanges, OpChangesWithCount, SearchNode from anki.decks import DeckId from anki.models import NotetypeId from anki.notes import Note, NoteFieldsCheckResult, NoteId @@ -289,18 +289,22 @@ class AddCards(QMainWindow): def _add_current_note(self) -> None: note = self.editor.note + # Prevent adding a note that has already been added (e.g., from double-clicking) + if note.id != 0: + return + if not self._note_can_be_added(note): return target_deck_id = self.deck_chooser.selected_deck_id - def on_success(changes: OpChanges) -> None: + def on_success(changes: OpChangesWithCount) -> None: # only used for detecting changed sticky fields on close self._last_added_note = note self.addHistory(note) - tooltip(tr.adding_added(), period=500) + tooltip(tr.importing_cards_added(count=changes.count), period=500) av_player.stop_and_clear_queue() self._load_new_note(sticky_fields_from=note) gui_hooks.add_cards_did_add_note(note) diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index e222f62c2..d935905f6 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -521,7 +521,7 @@ class Browser(QMainWindow): self.search() def current_search(self) -> str: - return self._line_edit().text() + return self._line_edit().text().replace("\n", " ") def search(self) -> None: """Search triggered programmatically. Caller must have saved note first.""" diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py index 4c9a97fb8..61096b5b3 100644 --- a/qt/aqt/browser/previewer.py +++ b/qt/aqt/browser/previewer.py @@ -13,7 +13,7 @@ import aqt.browser from anki.cards import Card from anki.collection import Config from anki.tags import MARKED_TAG -from aqt import AnkiQt, gui_hooks +from aqt import AnkiQt, gui_hooks, is_mac from aqt.qt import ( QCheckBox, QDialog, @@ -81,10 +81,15 @@ class Previewer(QDialog): qconnect(self.finished, self._on_finished) self.silentlyClose = True self.vbox = QVBoxLayout() + spacing = 6 self.vbox.setContentsMargins(0, 0, 0, 0) + self.vbox.setSpacing(spacing) self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER) self.vbox.addWidget(self._web) self.bbox = QDialogButtonBox() + self.bbox.setContentsMargins( + spacing, spacing if is_mac else 0, spacing, spacing + ) self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight) gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER) diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py index ce5ccb62f..b51910d4b 100644 --- a/qt/aqt/browser/sidebar/item.py +++ b/qt/aqt/browser/sidebar/item.py @@ -80,7 +80,7 @@ class SidebarItem: self.search_node = search_node self.on_expanded = on_expanded self.children: list[SidebarItem] = [] - self.tooltip: str | None = None + self.tooltip: str = name self._parent_item: SidebarItem | None = None self._expanded = expanded self._row_in_parent: int | None = None diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index f2f267097..e86f5fe96 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -151,6 +151,7 @@ class Editor: self.add_webview() self.setupWeb() self.setupShortcuts() + self.setupColourPalette() gui_hooks.editor_did_init(self) # Initial setup @@ -349,6 +350,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too keys, fn, _ = row QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore + def setupColourPalette(self) -> None: + if not (colors := self.mw.col.get_config("customColorPickerPalette")): + return + for i, colour in enumerate(colors[: QColorDialog.customCount()]): + if not QColor.isValidColorName(colour): + continue + QColorDialog.setCustomColor(i, QColor.fromString(colour)) + def _addFocusCheck(self, fn: Callable) -> Callable: def checkFocus() -> None: if self.currentField is None: diff --git a/qt/aqt/forms/filtered_deck.ui b/qt/aqt/forms/filtered_deck.ui index 0a90c40e5..a64a3968a 100644 --- a/qt/aqt/forms/filtered_deck.ui +++ b/qt/aqt/forms/filtered_deck.ui @@ -85,11 +85,11 @@ - - - 60 - 16777215 - + + + 0 + 0 + 1 @@ -168,11 +168,11 @@ - - - 60 - 16777215 - + + + 0 + 0 + 1 diff --git a/qt/aqt/forms/finddupes.ui b/qt/aqt/forms/finddupes.ui index 9a7c44c06..9bc8be87b 100644 --- a/qt/aqt/forms/finddupes.ui +++ b/qt/aqt/forms/finddupes.ui @@ -47,6 +47,9 @@ QComboBox::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon + diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 820e762d9..bedf23e5b 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -170,13 +170,42 @@ def favicon() -> Response: def _mime_for_path(path: str) -> str: "Mime type for provided path/filename." - if path.endswith(".css"): - # some users may have invalid mime type in the Windows registry - return "text/css" - elif path.endswith(".js") or path.endswith(".mjs"): - return "application/javascript" + + _, ext = os.path.splitext(path) + ext = ext.lower() + + # Badly-behaved apps on Windows can alter the standard mime types in the registry, which can completely + # break Anki's UI. So we hard-code the most common extensions. + mime_types = { + ".css": "text/css", + ".js": "application/javascript", + ".mjs": "application/javascript", + ".html": "text/html", + ".htm": "text/html", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".ico": "image/x-icon", + ".json": "application/json", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".otf": "font/otf", + ".mp3": "audio/mpeg", + ".mp4": "video/mp4", + ".webm": "video/webm", + ".ogg": "audio/ogg", + ".pdf": "application/pdf", + ".txt": "text/plain", + } + + if mime := mime_types.get(ext): + return mime else: - # autodetect + # fallback to mimetypes, which may consult the registry mime, _encoding = mimetypes.guess_type(path) return mime or "application/octet-stream" @@ -599,6 +628,15 @@ def deck_options_ready() -> bytes: return b"" +def save_custom_colours() -> bytes: + colors = [ + QColorDialog.customColor(i).name(QColor.NameFormat.HexRgb) + for i in range(QColorDialog.customCount()) + ] + aqt.mw.col.set_config("customColorPickerPalette", colors) + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -614,12 +652,14 @@ post_handler_list = [ search_in_browser, deck_options_require_close, deck_options_ready, + save_custom_colours, ] exposed_backend_list = [ # CollectionService "latest_progress", + "get_custom_colours", # DeckService "get_deck_names", # I18nService diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index 4a27c1e21..e36822553 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -18,7 +18,7 @@ def add_note( parent: QWidget, note: Note, target_deck_id: DeckId, -) -> CollectionOp[OpChanges]: +) -> CollectionOp[OpChangesWithCount]: return CollectionOp(parent, lambda col: col.add_note(note, target_deck_id)) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index afce6d489..939dd8c2c 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -260,6 +260,7 @@ class Preferences(QDialog): self.update_login_status() self.confirm_sync_after_login() + self.update_network() sync_login(self.mw, on_success) def sync_logout(self) -> None: diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a8839c598..6d68f9e3a 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -17,6 +17,7 @@ import aqt.browser import aqt.operations from anki.cards import Card, CardId from anki.collection import Config, OpChanges, OpChangesWithCount +from anki.lang import with_collapsed_whitespace from anki.scheduler.base import ScheduleCardsAsNew from anki.scheduler.v3 import ( CardAnswer, @@ -966,11 +967,15 @@ timerStopped = false; elapsed = self.mw.col.timeboxReached() if elapsed: assert not isinstance(elapsed, bool) - part1 = tr.studying_card_studied_in(count=elapsed[1]) - mins = int(round(elapsed[0] / 60)) - part2 = tr.studying_minute(count=mins) + cards_val = elapsed[1] + minutes_val = int(round(elapsed[0] / 60)) + message = with_collapsed_whitespace( + tr.studying_card_studied_in_minute( + cards=cards_val, minutes=str(minutes_val) + ) + ) fin = tr.studying_finish() - diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin]) + diag = askUserDialog(message, [tr.studying_continue(), fin]) diag.setIcon(QMessageBox.Icon.Information) if diag.run() == fin: self.mw.moveToState("deckBrowser") diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index e62786871..f54ebd3e8 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -32,6 +32,7 @@ from aqt._macos_helper import macos_helper from aqt.mpv import MPV, MPVBase, MPVCommandError from aqt.qt import * from aqt.taskman import TaskManager +from aqt.theme import theme_manager from aqt.utils import ( disable_help_button, restoreGeom, @@ -630,18 +631,44 @@ class QtAudioInputRecorder(Recorder): self.mw = mw self._parent = parent - from PyQt6.QtMultimedia import QAudioFormat, QAudioSource # type: ignore + from PyQt6.QtMultimedia import QAudioSource, QMediaDevices # type: ignore - format = QAudioFormat() - format.setChannelCount(2) - format.setSampleRate(44100) - format.setSampleFormat(QAudioFormat.SampleFormat.Int16) + # Get the default audio input device + device = QMediaDevices.defaultAudioInput() - source = QAudioSource(format, parent) + # Try to use Int16 format first (avoids conversion) + preferred_format = device.preferredFormat() + int16_format = preferred_format + int16_format.setSampleFormat(preferred_format.SampleFormat.Int16) + if device.isFormatSupported(int16_format): + # Use Int16 if supported + format = int16_format + else: + # Fall back to device's preferred format + format = preferred_format + + # Create the audio source with the chosen format + source = QAudioSource(device, format, parent) + + # Store the actual format being used self._format = source.format() self._audio_input = source + def _convert_float_to_int16(self, float_buffer: bytearray) -> bytes: + """Convert float32 audio samples to int16 format for WAV output.""" + import struct + + float_count = len(float_buffer) // 4 # 4 bytes per float32 + floats = struct.unpack(f"{float_count}f", float_buffer) + + # Convert to int16 range, clipping and scaling in one step + int16_samples = [ + max(-32768, min(32767, int(max(-1.0, min(1.0, f)) * 32767))) for f in floats + ] + + return struct.pack(f"{len(int16_samples)}h", *int16_samples) + def start(self, on_done: Callable[[], None]) -> None: self._iodevice = self._audio_input.start() self._buffer = bytearray() @@ -664,18 +691,32 @@ class QtAudioInputRecorder(Recorder): return def write_file() -> None: - # swallow the first 300ms to allow audio device to quiesce - wait = int(44100 * self.STARTUP_DELAY) - if len(self._buffer) <= wait: - return - self._buffer = self._buffer[wait:] + from PyQt6.QtMultimedia import QAudioFormat - # write out the wave file + # swallow the first 300ms to allow audio device to quiesce + bytes_per_frame = self._format.bytesPerFrame() + frames_to_skip = int(self._format.sampleRate() * self.STARTUP_DELAY) + bytes_to_skip = frames_to_skip * bytes_per_frame + + if len(self._buffer) <= bytes_to_skip: + return + self._buffer = self._buffer[bytes_to_skip:] + + # Check if we need to convert float samples to int16 + if self._format.sampleFormat() == QAudioFormat.SampleFormat.Float: + audio_data = self._convert_float_to_int16(self._buffer) + sample_width = 2 # int16 is 2 bytes + else: + # For integer formats, use the data as-is + audio_data = bytes(self._buffer) + sample_width = self._format.bytesPerSample() + + # write out the wave file with the correct format parameters wf = wave.open(self.output_path, "wb") wf.setnchannels(self._format.channelCount()) - wf.setsampwidth(2) + wf.setsampwidth(sample_width) wf.setframerate(self._format.sampleRate()) - wf.writeframes(self._buffer) + wf.writeframes(audio_data) wf.close() def and_then(fut: Future) -> None: @@ -743,7 +784,7 @@ class RecordDialog(QDialog): def _setup_dialog(self) -> None: self.setWindowTitle("Anki") icon = QLabel() - qicon = QIcon("icons:media-record.svg") + qicon = theme_manager.icon_from_resources("icons:media-record.svg") icon.setPixmap(qicon.pixmap(60, 60)) self.label = QLabel("...") hbox = QHBoxLayout() diff --git a/qt/aqt/stylesheets.py b/qt/aqt/stylesheets.py index 6b4eff1f5..6817b7063 100644 --- a/qt/aqt/stylesheets.py +++ b/qt/aqt/stylesheets.py @@ -180,7 +180,7 @@ class CustomStyles: QPushButton {{ margin: 1px; }} - QPushButton:focus {{ + QPushButton:focus, QPushButton:default:hover {{ border: 2px solid {tm.var(colors.BORDER_FOCUS)}; outline: none; margin: 0px; @@ -199,9 +199,6 @@ class CustomStyles: ) }; }} - QPushButton:default:hover {{ - border-width: 2px; - }} QPushButton:pressed, QPushButton:checked, QSpinBox::up-button:pressed, diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 9b29ada20..75bdeca89 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -209,11 +209,20 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt, label: str) -> None: return sync_progress = progress.full_sync + # If we've reached total, show the "checking" label if sync_progress.transferred == sync_progress.total: label = tr.sync_checking() + + total = sync_progress.total + transferred = sync_progress.transferred + + # Scale both to kilobytes with floor division + max_for_bar = total // 1024 + value_for_bar = transferred // 1024 + mw.progress.update( - value=sync_progress.transferred, - max=sync_progress.total, + value=value_for_bar, + max=max_for_bar, process=False, label=label, ) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 675eb9345..1e7b1f568 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -115,7 +115,7 @@ class ThemeManager: # Workaround for Qt bug. First attempt was percent-escaping the chars, # but Qt can't handle that. # https://forum.qt.io/topic/55274/solved-qss-with-special-characters/11 - path = re.sub(r"([\u00A1-\u00FF])", r"\\\1", path) + path = re.sub(r"(['\u00A1-\u00FF])", r"\\\1", path) return path def icon_from_resources(self, path: str | ColoredIcon) -> QIcon: diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index d559fb41f..f77e5c975 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -94,8 +94,15 @@ class TTSPlayer: rank -= 1 - # if no preferred voices match, we fall back on language - # with a rank of -100 + # if no requested voices match, use a preferred fallback voice + # (for example, Apple Samantha) with rank of -50 + for avail in avail_voices: + if avail.lang == tag.lang: + if avail.lang == "en_US" and avail.name.startswith("Apple_Samantha"): + return TTSVoiceMatch(voice=avail, rank=-50) + + # if no requested or preferred voices match, we fall back on + # the first available voice for the language, with a rank of -100 for avail in avail_voices: if avail.lang == tag.lang: return TTSVoiceMatch(voice=avail, rank=-100) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 43efc513f..ae88dadcb 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -809,7 +809,7 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: wsize = widget.size() cappedWidth = min(geom.width(), wsize.width()) cappedHeight = min(geom.height(), wsize.height()) - if cappedWidth > wsize.width() or cappedHeight > wsize.height(): + if cappedWidth < wsize.width() or cappedHeight < wsize.height(): widget.resize(QSize(cappedWidth, cappedHeight)) # ensure widget is inside top left diff --git a/qt/launcher/Cargo.toml b/qt/launcher/Cargo.toml index 7de321a29..5fd1c9900 100644 --- a/qt/launcher/Cargo.toml +++ b/qt/launcher/Cargo.toml @@ -8,11 +8,13 @@ publish = false rust-version.workspace = true [dependencies] +anki_i18n.workspace = true anki_io.workspace = true anki_process.workspace = true anyhow.workspace = true camino.workspace = true dirs.workspace = true +locale_config.workspace = true serde_json.workspace = true [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] diff --git a/qt/launcher/build.rs b/qt/launcher/build.rs index 3ba75b0e1..bc30f8dff 100644 --- a/qt/launcher/build.rs +++ b/qt/launcher/build.rs @@ -7,4 +7,7 @@ fn main() { .manifest_required() .unwrap(); } + 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}"); } diff --git a/qt/launcher/mac/build.sh b/qt/launcher/mac/build.sh index 6143451b4..b861bc006 100755 --- a/qt/launcher/mac/build.sh +++ b/qt/launcher/mac/build.sh @@ -30,6 +30,12 @@ lipo -create \ -output "$APP_LAUNCHER/Contents/MacOS/launcher" cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/" +# Build install_name_tool stub +clang -arch arm64 -o "$OUTPUT_DIR/stub_arm64" stub.c +clang -arch x86_64 -o "$OUTPUT_DIR/stub_x86_64" stub.c +lipo -create "$OUTPUT_DIR/stub_arm64" "$OUTPUT_DIR/stub_x86_64" -output "$APP_LAUNCHER/Contents/MacOS/install_name_tool" +rm "$OUTPUT_DIR/stub_arm64" "$OUTPUT_DIR/stub_x86_64" + # Copy support files ANKI_VERSION=$(cat ../../../.version | tr -d '\n') sed "s/ANKI_VERSION/$ANKI_VERSION/g" Info.plist > "$APP_LAUNCHER/Contents/Info.plist" @@ -40,7 +46,7 @@ cp ../versions.py "$APP_LAUNCHER/Contents/Resources/" # Codesign/bundle if [ -z "$NODMG" ]; then - for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do + for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/install_name_tool" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do codesign --force -vvvv -o runtime -s "Developer ID Application:" \ --entitlements entitlements.python.xml \ "$i" diff --git a/qt/launcher/mac/stub.c b/qt/launcher/mac/stub.c new file mode 100644 index 000000000..09f1479a7 --- /dev/null +++ b/qt/launcher/mac/stub.c @@ -0,0 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +int main(void) { + return 0; +} \ No newline at end of file diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 297df5b8b..dab9435ea 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -10,6 +10,7 @@ use std::process::Command; use std::time::SystemTime; use std::time::UNIX_EPOCH; +use anki_i18n::I18n; use anki_io::copy_file; use anki_io::create_dir_all; use anki_io::modified_time; @@ -31,6 +32,7 @@ use crate::platform::respawn_launcher; mod platform; struct State { + tr: I18n, current_version: Option, prerelease_marker: std::path::PathBuf, uv_install_root: std::path::PathBuf, @@ -51,6 +53,8 @@ struct State { previous_version: Option, resources_dir: std::path::PathBuf, venv_folder: std::path::PathBuf, + /// system Python + PyQt6 library mode + system_qt: bool, } #[derive(Debug, Clone)] @@ -88,13 +92,24 @@ fn main() { } fn run() -> Result<()> { - let uv_install_root = dirs::data_local_dir() - .context("Unable to determine data_dir")? - .join("AnkiProgramFiles"); + 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 locale = locale_config::Locale::user_default().to_string(); + let mut state = State { + tr: I18n::new(&[if !locale.is_empty() { + locale + } else { + "en".to_owned() + }]), current_version: None, prerelease_marker: uv_install_root.join("prerelease"), uv_install_root: uv_install_root.clone(), @@ -113,6 +128,8 @@ fn run() -> Result<()> { 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"), }; @@ -135,7 +152,9 @@ fn run() -> Result<()> { 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; - if !launcher_requested && !pyproject_has_changed { + let different_launcher = diff_launcher_was_installed(&state)?; + + if !launcher_requested && !pyproject_has_changed && !different_launcher { // If no launcher request and venv is already up to date, launch Anki normally let args: Vec = std::env::args().skip(1).collect(); let cmd = build_python_command(&state, &args)?; @@ -152,10 +171,12 @@ fn run() -> Result<()> { } print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top - println!("\x1B[1mAnki Launcher\x1B[0m\n"); + println!("\x1B[1m{}\x1B[0m\n", state.tr.launcher_title()); ensure_os_supported()?; + println!("{}\n", state.tr.launcher_press_enter_to_install()); + check_versions(&mut state); main_menu_loop(&state)?; @@ -170,15 +191,18 @@ fn run() -> Result<()> { } if cfg!(unix) && !cfg!(target_os = "macos") { - println!("\nPress enter to start Anki."); + println!("\n{}", state.tr.launcher_press_enter_to_start()); let mut input = String::new(); let _ = stdin().read_line(&mut input); } else { // on Windows/macOS, the user needs to close the terminal/console // currently, but ideas on how we can avoid this would be good! println!(); - println!("Anki will start shortly."); - println!("\x1B[1mYou can close this window.\x1B[0m\n"); + println!("{}", state.tr.launcher_anki_will_start_shortly()); + println!( + "\x1B[1m{}\x1B[0m\n", + state.tr.launcher_you_can_close_this_window() + ); } // respawn the launcher as a disconnected subprocess for normal startup @@ -193,8 +217,8 @@ fn extract_aqt_version(state: &State) -> Option { return None; } - let output = Command::new(&state.uv_path) - .current_dir(&state.uv_install_root) + let output = uv_command(state) + .ok()? .env("VIRTUAL_ENV", &state.venv_folder) .args(["pip", "show", "aqt"]) .output() @@ -250,7 +274,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re // Remove sync marker before attempting sync let _ = remove_file(&state.sync_complete_marker); - println!("Updating Anki...\n"); + println!("{}\n", state.tr.launcher_updating_anki()); let python_version_trimmed = if state.user_python_version_path.exists() { let python_version = read_file(&state.user_python_version_path)?; @@ -261,52 +285,64 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re None }; - let have_venv = state.venv_folder.exists(); - if cfg!(target_os = "macos") && !have_developer_tools() && !have_venv { - println!("If you see a pop-up about 'install_name_tool', you can cancel it, and ignore the warning below.\n"); - } - // Prepare to sync the venv - let mut command = Command::new(&state.uv_path); - command.current_dir(&state.uv_install_root); + let mut command = uv_command(state)?; - // remove UV_* environment variables to avoid interference - for (key, _) in std::env::vars() { - if key.starts_with("UV_") || key == "VIRTUAL_ENV" { - command.env_remove(key); + 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); + } } } - // remove CONDA_PREFIX/bin from PATH to avoid conda interference - #[cfg(target_os = "macos")] - 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); - } + // 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) .env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir) .env( "UV_HTTP_TIMEOUT", std::env::var("UV_HTTP_TIMEOUT").unwrap_or_else(|_| "180".to_string()), - ) - .args(["sync", "--upgrade", "--managed-python", "--no-config"]); + ); - // Add python version if .python-version file exists - if let Some(version) = &python_version_trimmed { - command.args(["--python", version]); + command.args(["sync", "--upgrade", "--no-config"]); + if !state.system_qt { + command.arg("--managed-python"); } - if state.no_cache_marker.exists() { - command.env("UV_NO_CACHE", "1"); + // 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]); + } } match command.ensure_success() { @@ -353,10 +389,10 @@ fn main_menu_loop(state: &State) -> Result<()> { // Toggle beta prerelease file if state.prerelease_marker.exists() { let _ = remove_file(&state.prerelease_marker); - println!("Beta releases disabled."); + println!("{}", state.tr.launcher_beta_releases_disabled()); } else { write_file(&state.prerelease_marker, "")?; - println!("Beta releases enabled."); + println!("{}", state.tr.launcher_beta_releases_enabled()); } println!(); continue; @@ -365,14 +401,14 @@ fn main_menu_loop(state: &State) -> Result<()> { // Toggle cache disable file if state.no_cache_marker.exists() { let _ = remove_file(&state.no_cache_marker); - println!("Download caching enabled."); + println!("{}", state.tr.launcher_download_caching_enabled()); } 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); } - println!("Download caching disabled and cache cleared."); + println!("{}", state.tr.launcher_download_caching_disabled()); } println!(); continue; @@ -415,44 +451,62 @@ fn file_timestamp_secs(path: &std::path::Path) -> i64 { fn get_main_menu_choice(state: &State) -> Result { loop { - println!("1) Latest Anki (press Enter)"); - println!("2) Choose a version"); + println!("1) {}", state.tr.launcher_latest_anki()); + println!("2) {}", state.tr.launcher_choose_a_version()); if let Some(current_version) = &state.current_version { let normalized_current = normalize_version(current_version); if state.pyproject_modified_by_user { - println!("3) Sync project changes"); + println!("3) {}", state.tr.launcher_sync_project_changes()); } else { - println!("3) Keep existing version ({normalized_current})"); + println!( + "3) {}", + state.tr.launcher_keep_existing_version(normalized_current) + ); } } if let Some(prev_version) = &state.previous_version { if state.current_version.as_ref() != Some(prev_version) { let normalized_prev = normalize_version(prev_version); - println!("4) Revert to previous version ({normalized_prev})"); + println!( + "4) {}", + state.tr.launcher_revert_to_previous(normalized_prev) + ); } } println!(); let betas_enabled = state.prerelease_marker.exists(); println!( - "5) Allow betas: {}", - if betas_enabled { "on" } else { "off" } + "5) {}", + state.tr.launcher_allow_betas(if betas_enabled { + state.tr.launcher_on() + } else { + state.tr.launcher_off() + }) ); let cache_enabled = !state.no_cache_marker.exists(); println!( - "6) Cache downloads: {}", - if cache_enabled { "on" } else { "off" } + "6) {}", + state.tr.launcher_cache_downloads(if cache_enabled { + state.tr.launcher_on() + } else { + state.tr.launcher_off() + }) ); let mirror_enabled = is_mirror_enabled(state); println!( - "7) Download mirror: {}", - if mirror_enabled { "on" } else { "off" } + "7) {}", + state.tr.launcher_download_mirror(if mirror_enabled { + state.tr.launcher_on() + } else { + state.tr.launcher_off() + }) ); println!(); - println!("8) Uninstall"); + println!("8) {}", state.tr.launcher_uninstall()); print!("> "); let _ = stdout().flush(); @@ -474,7 +528,7 @@ fn get_main_menu_choice(state: &State) -> Result { if state.current_version.is_some() { MainMenuChoice::KeepExisting } else { - println!("Invalid input. Please try again.\n"); + println!("{}\n", state.tr.launcher_invalid_input()); continue; } } @@ -486,7 +540,7 @@ fn get_main_menu_choice(state: &State) -> Result { } } } - println!("Invalid input. Please try again.\n"); + println!("{}\n", state.tr.launcher_invalid_input()); continue; } "5" => MainMenuChoice::ToggleBetas, @@ -494,7 +548,7 @@ fn get_main_menu_choice(state: &State) -> Result { "7" => MainMenuChoice::DownloadMirror, "8" => MainMenuChoice::Uninstall, _ => { - println!("Invalid input. Please try again."); + println!("{}\n", state.tr.launcher_invalid_input()); continue; } }); @@ -509,9 +563,9 @@ fn get_version_kind(state: &State) -> Result> { .map(|v| v.as_str()) .collect::>() .join(", "); - println!("Latest releases: {releases_str}"); + println!("{}", state.tr.launcher_latest_releases(releases_str)); - println!("Enter the version you want to install:"); + println!("{}", state.tr.launcher_enter_the_version_you_want()); print!("> "); let _ = stdout().flush(); @@ -535,29 +589,38 @@ fn get_version_kind(state: &State) -> Result> { Ok(Some(version_kind)) } (None, true) => { - println!("Versions before 2.1.50 can't be installed."); + println!("{}", state.tr.launcher_versions_before_cant_be_installed()); Ok(None) } _ => { - println!("Invalid version.\n"); + println!("{}\n", state.tr.launcher_invalid_version()); Ok(None) } } } fn with_only_latest_patch(versions: &[String]) -> Vec { - // Only show the latest patch release for a given (major, minor) + // Assumes versions are sorted in descending order (newest first) + // Only show the latest patch release for a given (major, minor), + // and exclude pre-releases if a newer major_minor exists let mut seen_major_minor = std::collections::HashSet::new(); versions .iter() .filter(|v| { - let (major, minor, _, _) = parse_version_for_filtering(v); + let (major, minor, _, is_prerelease) = parse_version_for_filtering(v); if major == 2 { return true; } let major_minor = (major, minor); if seen_major_minor.contains(&major_minor) { false + } else if is_prerelease + && seen_major_minor + .iter() + .any(|&(seen_major, seen_minor)| (seen_major, seen_minor) > (major, minor)) + { + // Exclude pre-release if a newer major_minor exists + false } else { seen_major_minor.insert(major_minor); true @@ -658,9 +721,8 @@ fn filter_and_normalize_versions( fn fetch_versions(state: &State) -> Result> { let versions_script = state.resources_dir.join("versions.py"); - let mut cmd = Command::new(&state.uv_path); - cmd.current_dir(&state.uv_install_root) - .args(["run", "--no-project", "--no-config", "--managed-python"]) + 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)?; @@ -676,7 +738,7 @@ fn fetch_versions(state: &State) -> Result> { let output = match cmd.utf8_output() { Ok(output) => output, Err(e) => { - print!("Unable to check for Anki versions. Please check your internet connection.\n\n"); + print!("{}\n\n", state.tr.launcher_unable_to_check_for_versions()); return Err(e.into()); } }; @@ -685,7 +747,7 @@ fn fetch_versions(state: &State) -> Result> { } fn get_releases(state: &State) -> Result { - println!("Checking for updates..."); + println!("{}", state.tr.launcher_checking_for_updates()); let include_prereleases = state.prerelease_marker.exists(); let all_versions = fetch_versions(state)?; let all_versions = filter_and_normalize_versions(all_versions, include_prereleases); @@ -726,9 +788,20 @@ fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> { ), }; - // Add mirror configuration if enabled - let final_content = if let Some((python_mirror, pypi_mirror)) = get_mirror_urls(state)? { - format!("{updated_content}\n\n[[tool.uv.index]]\nname = \"mirror\"\nurl = \"{pypi_mirror}\"\ndefault = true\n\n[tool.uv]\npython-install-mirror = \"{python_mirror}\"\n") + 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 }; @@ -876,7 +949,7 @@ fn get_anki_addons21_path() -> Result { } fn handle_uninstall(state: &State) -> Result { - println!("Uninstall Anki's program files? (y/n)"); + println!("{}", state.tr.launcher_uninstall_confirm()); print!("> "); let _ = stdout().flush(); @@ -885,7 +958,7 @@ fn handle_uninstall(state: &State) -> Result { let input = input.trim().to_lowercase(); if input != "y" { - println!("Uninstall cancelled."); + println!("{}", state.tr.launcher_uninstall_cancelled()); println!(); return Ok(false); } @@ -893,11 +966,11 @@ fn handle_uninstall(state: &State) -> Result { // Remove program files if state.uv_install_root.exists() { anki_io::remove_dir_all(&state.uv_install_root)?; - println!("Program files removed."); + println!("{}", state.tr.launcher_program_files_removed()); } println!(); - println!("Remove all profiles/cards? (y/n)"); + println!("{}", state.tr.launcher_remove_all_profiles_confirm()); print!("> "); let _ = stdout().flush(); @@ -907,7 +980,7 @@ fn handle_uninstall(state: &State) -> Result { if input == "y" && state.anki_base_folder.exists() { anki_io::remove_dir_all(&state.anki_base_folder)?; - println!("User data removed."); + println!("{}", state.tr.launcher_user_data_removed()); } println!(); @@ -925,12 +998,37 @@ fn handle_uninstall(state: &State) -> Result { Ok(true) } -fn have_developer_tools() -> bool { - Command::new("xcode-select") - .args(["-p"]) - .output() - .map(|output| output.status.success()) - .unwrap_or(false) +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); + } + + if state.no_cache_marker.exists() { + command.env("UV_NO_CACHE", "1"); + } else { + 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"); + + Ok(command) } fn build_python_command(state: &State, args: &[String]) -> Result { @@ -955,6 +1053,7 @@ fn build_python_command(state: &State, args: &[String]) -> Result { // 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) } @@ -984,9 +1083,9 @@ fn get_mirror_urls(state: &State) -> Result> { fn show_mirror_submenu(state: &State) -> Result<()> { loop { - println!("Download mirror options:"); - println!("1) No mirror"); - println!("2) China"); + println!("{}", state.tr.launcher_download_mirror_options()); + println!("1) {}", state.tr.launcher_mirror_no_mirror()); + println!("2) {}", state.tr.launcher_mirror_china()); print!("> "); let _ = stdout().flush(); @@ -1000,14 +1099,14 @@ fn show_mirror_submenu(state: &State) -> Result<()> { if state.mirror_path.exists() { let _ = remove_file(&state.mirror_path); } - println!("Mirror disabled."); + println!("{}", state.tr.launcher_mirror_disabled()); break; } "2" => { // Write China mirror URLs 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)?; - println!("China mirror enabled."); + println!("{}", state.tr.launcher_mirror_china_enabled()); break; } "" => { @@ -1015,7 +1114,7 @@ fn show_mirror_submenu(state: &State) -> Result<()> { break; } _ => { - println!("Invalid input. Please try again."); + println!("{}", state.tr.launcher_invalid_input()); continue; } } @@ -1023,6 +1122,20 @@ fn show_mirror_submenu(state: &State) -> Result<()> { Ok(()) } +fn diff_launcher_was_installed(state: &State) -> 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) +} + #[cfg(test)] mod tests { use super::*; diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs index f97d7fd07..3f5b0ce2e 100644 --- a/qt/launcher/src/platform/mac.rs +++ b/qt/launcher/src/platform/mac.rs @@ -62,8 +62,9 @@ pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result< pub fn relaunch_in_terminal() -> Result<()> { let current_exe = std::env::current_exe().context("Failed to get current executable path")?; Command::new("open") - .args(["-a", "Terminal"]) + .args(["-na", "Terminal"]) .arg(current_exe) + .env_remove("ANKI_LAUNCHER_WANT_TERMINAL") .ensure_spawn()?; std::process::exit(0); } diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 6a582f1aa..eec7634f1 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -134,5 +134,8 @@ 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/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index ebdff6261..d20c9a8b4 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -38,6 +38,26 @@ fn is_windows_10() -> bool { } } +/// 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 ensure_terminal_shown() -> Result<()> { unsafe { if !GetConsoleWindow().is_invalid() { diff --git a/rslib/i18n/Cargo.toml b/rslib/i18n/Cargo.toml index 899680cc3..cce8bfe6f 100644 --- a/rslib/i18n/Cargo.toml +++ b/rslib/i18n/Cargo.toml @@ -22,6 +22,7 @@ inflections.workspace = true anki_io.workspace = true anyhow.workspace = true itertools.workspace = true +regex.workspace = true [dependencies] fluent.workspace = true diff --git a/rslib/i18n/build.rs b/rslib/i18n/build.rs index 4baa6a709..f604c9167 100644 --- a/rslib/i18n/build.rs +++ b/rslib/i18n/build.rs @@ -23,10 +23,10 @@ use write_strings::write_strings; fn main() -> Result<()> { // generate our own requirements - let map = get_ftl_data(); + let mut map = get_ftl_data(); check(&map); - let modules = get_modules(&map); - write_strings(&map, &modules); + let mut modules = get_modules(&map); + write_strings(&map, &modules, "strings.rs", "All"); typescript::write_ts_interface(&modules)?; python::write_py_interface(&modules)?; @@ -41,5 +41,12 @@ fn main() -> Result<()> { write_file_if_changed(path, meta_json)?; } } + + // generate strings for the launcher + map.iter_mut() + .for_each(|(_, modules)| modules.retain(|module, _| module == "launcher")); + modules.retain(|module| module.name == "launcher"); + write_strings(&map, &modules, "strings_launcher.rs", "Launcher"); + Ok(()) } diff --git a/rslib/i18n/src/generated.rs b/rslib/i18n/src/generated.rs index f3526f79f..7463a594e 100644 --- a/rslib/i18n/src/generated.rs +++ b/rslib/i18n/src/generated.rs @@ -1,9 +1,15 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -// Include auto-generated content - #![allow(clippy::all)] -#![allow(text_direction_codepoint_in_literal)] +#[derive(Clone)] +pub struct All; + +// Include auto-generated content include!(concat!(env!("OUT_DIR"), "/strings.rs")); + +impl Translations for All { + const STRINGS: &phf::Map<&str, &phf::Map<&str, &str>> = &_STRINGS; + const KEYS_BY_MODULE: &[&[&str]] = &_KEYS_BY_MODULE; +} diff --git a/rslib/i18n/src/generated_launcher.rs b/rslib/i18n/src/generated_launcher.rs new file mode 100644 index 000000000..35dc3f28b --- /dev/null +++ b/rslib/i18n/src/generated_launcher.rs @@ -0,0 +1,15 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#![allow(clippy::all)] + +#[derive(Clone)] +pub struct Launcher; + +// Include auto-generated content +include!(concat!(env!("OUT_DIR"), "/strings_launcher.rs")); + +impl Translations for Launcher { + const STRINGS: &phf::Map<&str, &phf::Map<&str, &str>> = &_STRINGS; + const KEYS_BY_MODULE: &[&[&str]] = &_KEYS_BY_MODULE; +} diff --git a/rslib/i18n/src/lib.rs b/rslib/i18n/src/lib.rs index bfd6f5ba2..95b960fad 100644 --- a/rslib/i18n/src/lib.rs +++ b/rslib/i18n/src/lib.rs @@ -2,8 +2,10 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod generated; +mod generated_launcher; use std::borrow::Cow; +use std::marker::PhantomData; use std::sync::Arc; use std::sync::Mutex; @@ -12,8 +14,6 @@ use fluent::FluentArgs; use fluent::FluentResource; use fluent::FluentValue; use fluent_bundle::bundle::FluentBundle as FluentBundleOrig; -use generated::KEYS_BY_MODULE; -use generated::STRINGS; use num_format::Locale; use serde::Serialize; use unic_langid::LanguageIdentifier; @@ -22,6 +22,9 @@ type FluentBundle = FluentBundleOrig { fn round(self) -> Self; } @@ -187,20 +190,67 @@ fn get_bundle_with_extra( get_bundle(text, extra_text, &locales) } +pub trait Translations { + const STRINGS: &phf::Map<&str, &phf::Map<&str, &str>>; + const KEYS_BY_MODULE: &[&[&str]]; +} + #[derive(Clone)] -pub struct I18n { +pub struct I18n { inner: Arc>, + _translations_type: std::marker::PhantomData

, } -fn get_key(module_idx: usize, translation_idx: usize) -> &'static str { - KEYS_BY_MODULE - .get(module_idx) - .and_then(|translations| translations.get(translation_idx)) - .cloned() - .unwrap_or("invalid-module-or-translation-index") -} +impl I18n

{ + fn get_key(module_idx: usize, translation_idx: usize) -> &'static str { + P::KEYS_BY_MODULE + .get(module_idx) + .and_then(|translations| translations.get(translation_idx)) + .cloned() + .unwrap_or("invalid-module-or-translation-index") + } + + fn get_modules(langs: &[LanguageIdentifier], desired_modules: &[String]) -> Vec { + langs + .iter() + .cloned() + .map(|lang| { + let mut buf = String::new(); + let lang_name = remapped_lang_name(&lang); + if let Some(strings) = P::STRINGS.get(lang_name) { + if desired_modules.is_empty() { + // empty list, provide all modules + for value in strings.values() { + buf.push_str(value) + } + } else { + for module_name in desired_modules { + if let Some(text) = strings.get(module_name.as_str()) { + buf.push_str(text); + } + } + } + } + buf + }) + .collect() + } + + /// This temporarily behaves like the older code; in the future we could + /// either access each &str separately, or load them on demand. + fn ftl_localized_text(lang: &LanguageIdentifier) -> Option { + let lang = remapped_lang_name(lang); + if let Some(module) = P::STRINGS.get(lang) { + let mut text = String::new(); + for module_text in module.values() { + text.push_str(module_text) + } + Some(text) + } else { + None + } + } -impl I18n { pub fn template_only() -> Self { Self::new::<&str>(&[]) } @@ -225,7 +275,7 @@ impl I18n { let mut output_langs = vec![]; for lang in input_langs { // if the language is bundled in the binary - if let Some(text) = ftl_localized_text(&lang).or_else(|| { + if let Some(text) = Self::ftl_localized_text(&lang).or_else(|| { // when testing, allow missing translations if cfg!(test) { Some(String::new()) @@ -244,7 +294,7 @@ impl I18n { // add English templates let template_lang = "en-US".parse().unwrap(); - let template_text = ftl_localized_text(&template_lang).unwrap(); + let template_text = Self::ftl_localized_text(&template_lang).unwrap(); let template_bundle = get_bundle_with_extra(&template_text, None).unwrap(); bundles.push(template_bundle); output_langs.push(template_lang); @@ -261,6 +311,7 @@ impl I18n { bundles, langs: output_langs, })), + _translations_type: PhantomData, } } @@ -270,7 +321,7 @@ impl I18n { message_index: usize, args: FluentArgs, ) -> String { - let key = get_key(module_index, message_index); + let key = Self::get_key(module_index, message_index); self.translate(key, Some(args)).into() } @@ -305,7 +356,7 @@ impl I18n { /// implementation. pub fn resources_for_js(&self, desired_modules: &[String]) -> ResourcesForJavascript { let inner = self.inner.lock().unwrap(); - let resources = get_modules(&inner.langs, desired_modules); + let resources = Self::get_modules(&inner.langs, desired_modules); ResourcesForJavascript { langs: inner.langs.iter().map(ToString::to_string).collect(), resources, @@ -313,47 +364,6 @@ impl I18n { } } -fn get_modules(langs: &[LanguageIdentifier], desired_modules: &[String]) -> Vec { - langs - .iter() - .cloned() - .map(|lang| { - let mut buf = String::new(); - let lang_name = remapped_lang_name(&lang); - if let Some(strings) = STRINGS.get(lang_name) { - if desired_modules.is_empty() { - // empty list, provide all modules - for value in strings.values() { - buf.push_str(value) - } - } else { - for module_name in desired_modules { - if let Some(text) = strings.get(module_name.as_str()) { - buf.push_str(text); - } - } - } - } - buf - }) - .collect() -} - -/// This temporarily behaves like the older code; in the future we could either -/// access each &str separately, or load them on demand. -fn ftl_localized_text(lang: &LanguageIdentifier) -> Option { - let lang = remapped_lang_name(lang); - if let Some(module) = STRINGS.get(lang) { - let mut text = String::new(); - for module_text in module.values() { - text.push_str(module_text) - } - Some(text) - } else { - None - } -} - struct I18nInner { // bundles in preferred language order, with template English as the // last element @@ -490,7 +500,7 @@ mod test { #[test] fn i18n() { // English template - let tr = I18n::new(&["zz"]); + let tr = I18n::::new(&["zz"]); assert_eq!(tr.translate("valid-key", None), "a valid key"); assert_eq!(tr.translate("invalid-key", None), "invalid-key"); @@ -513,7 +523,7 @@ mod test { ); // Another language - let tr = I18n::new(&["ja_JP"]); + let tr = I18n::::new(&["ja_JP"]); assert_eq!(tr.translate("valid-key", None), "キー"); assert_eq!(tr.translate("only-in-english", None), "not translated"); assert_eq!(tr.translate("invalid-key", None), "invalid-key"); @@ -524,7 +534,7 @@ mod test { ); // Decimal separator - let tr = I18n::new(&["pl-PL"]); + let tr = I18n::::new(&["pl-PL"]); // Polish will use a comma if the string is translated assert_eq!( tr.translate("one-arg-key", Some(tr_args!["one"=>2.07])), diff --git a/rslib/i18n/write_strings.rs b/rslib/i18n/write_strings.rs index 36af62eeb..db31be2b7 100644 --- a/rslib/i18n/write_strings.rs +++ b/rslib/i18n/write_strings.rs @@ -15,7 +15,7 @@ use crate::extract::VariableKind; use crate::gather::TranslationsByFile; use crate::gather::TranslationsByLang; -pub fn write_strings(map: &TranslationsByLang, modules: &[Module]) { +pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str, tag: &str) { let mut buf = String::new(); // lang->module map @@ -25,23 +25,25 @@ pub fn write_strings(map: &TranslationsByLang, modules: &[Module]) { // ordered list of translations by module write_translation_key_index(modules, &mut buf); // methods to generate messages - write_methods(modules, &mut buf); + write_methods(modules, &mut buf, tag); let dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let path = dir.join("strings.rs"); + let path = dir.join(out_fn); fs::write(path, buf).unwrap(); } -fn write_methods(modules: &[Module], buf: &mut String) { +fn write_methods(modules: &[Module], buf: &mut String, tag: &str) { buf.push_str( r#" -use crate::{I18n,Number}; +#[allow(unused_imports)] +use crate::{I18n,Number,Translations}; +#[allow(unused_imports)] use fluent::{FluentValue, FluentArgs}; use std::borrow::Cow; -impl I18n { "#, ); + writeln!(buf, "impl I18n<{tag}> {{").unwrap(); for module in modules { for translation in &module.translations { let func = translation.key.to_snake_case(); @@ -142,7 +144,7 @@ fn write_translation_key_index(modules: &[Module], buf: &mut String) { writeln!( buf, - "pub(crate) const KEYS_BY_MODULE: [&[&str]; {count}] = [", + "pub(crate) const _KEYS_BY_MODULE: [&[&str]; {count}] = [", count = modules.len(), ) .unwrap(); @@ -162,7 +164,7 @@ fn write_translation_key_index(modules: &[Module], buf: &mut String) { fn write_lang_map(map: &TranslationsByLang, buf: &mut String) { buf.push_str( " -pub(crate) const STRINGS: phf::Map<&str, &phf::Map<&str, &str>> = phf::phf_map! { +pub(crate) const _STRINGS: phf::Map<&str, &phf::Map<&str, &str>> = phf::phf_map! { ", ); @@ -195,12 +197,30 @@ pub(crate) const {lang_name}: phf::Map<&str, &str> = phf::phf_map! {{", .unwrap(); for (module, contents) in modules { - writeln!(buf, r###" "{module}" => r##"{contents}"##,"###).unwrap(); + let escaped_contents = escape_unicode_control_chars(contents); + writeln!( + buf, + r###" "{module}" => r##"{escaped_contents}"##,"### + ) + .unwrap(); } buf.push_str("};\n"); } +fn escape_unicode_control_chars(input: &str) -> String { + use regex::Regex; + + static RE: std::sync::OnceLock = std::sync::OnceLock::new(); + let re = RE.get_or_init(|| Regex::new(r"[\u{202a}-\u{202e}\u{2066}-\u{2069}]").unwrap()); + + re.replace_all(input, |caps: ®ex::Captures| { + let c = caps.get(0).unwrap().as_str().chars().next().unwrap(); + format!("\\u{{{:04x}}}", c as u32) + }) + .into_owned() +} + fn lang_constant_name(lang: &str) -> String { lang.to_ascii_uppercase().replace('-', "_") } diff --git a/rslib/linkchecker/tests/links.rs b/rslib/linkchecker/tests/links.rs index 2f39fbe31..39201de78 100644 --- a/rslib/linkchecker/tests/links.rs +++ b/rslib/linkchecker/tests/links.rs @@ -42,14 +42,14 @@ enum CheckableUrl { } impl CheckableUrl { - fn url(&self) -> Cow { + fn url(&self) -> Cow<'_, str> { match *self { Self::HelpPage(page) => help_page_to_link(page).into(), Self::String(s) => s.into(), } } - fn anchor(&self) -> Cow { + fn anchor(&self) -> Cow<'_, str> { match *self { Self::HelpPage(page) => help_page_link_suffix(page).into(), Self::String(s) => s.split('#').next_back().unwrap_or_default().into(), diff --git a/rslib/proto/python.rs b/rslib/proto/python.rs index a5adb4179..5c245de1d 100644 --- a/rslib/proto/python.rs +++ b/rslib/proto/python.rs @@ -22,7 +22,7 @@ pub(crate) fn write_python_interface(services: &[BackendService]) -> Result<()> write_header(&mut out)?; for service in services { - if service.name == "BackendAnkidroidService" { + if ["BackendAnkidroidService", "BackendFrontendService"].contains(&service.name.as_str()) { continue; } for method in service.all_methods() { diff --git a/rslib/src/backend/collection.rs b/rslib/src/backend/collection.rs index d9f7c6262..5cef74381 100644 --- a/rslib/src/backend/collection.rs +++ b/rslib/src/backend/collection.rs @@ -94,7 +94,7 @@ impl BackendCollectionService for Backend { } impl Backend { - pub(super) fn lock_open_collection(&self) -> Result>> { + pub(super) fn lock_open_collection(&self) -> Result>> { let guard = self.col.lock().unwrap(); guard .is_some() @@ -102,7 +102,7 @@ impl Backend { .ok_or(AnkiError::CollectionNotOpen) } - pub(super) fn lock_closed_collection(&self) -> Result>> { + pub(super) fn lock_closed_collection(&self) -> Result>> { let guard = self.col.lock().unwrap(); guard .is_none() diff --git a/rslib/src/card_rendering/mod.rs b/rslib/src/card_rendering/mod.rs index 3d61a4fe5..262f2a7c9 100644 --- a/rslib/src/card_rendering/mod.rs +++ b/rslib/src/card_rendering/mod.rs @@ -34,7 +34,7 @@ pub fn prettify_av_tags + AsRef>(txt: S) -> String { /// Parse `txt` into [CardNodes] and return the result, /// or [None] if it only contains text nodes. -fn nodes_or_text_only(txt: &str) -> Option { +fn nodes_or_text_only(txt: &str) -> Option> { let nodes = CardNodes::parse(txt); (!nodes.text_only).then_some(nodes) } diff --git a/rslib/src/card_rendering/parser.rs b/rslib/src/card_rendering/parser.rs index b124c069d..0ee66a9b1 100644 --- a/rslib/src/card_rendering/parser.rs +++ b/rslib/src/card_rendering/parser.rs @@ -103,13 +103,13 @@ fn is_not0<'parser, 'arr: 'parser, 's: 'parser>( move |s| alt((is_not(arr), success(""))).parse(s) } -fn node(s: &str) -> IResult { +fn node(s: &str) -> IResult<'_, Node<'_>> { alt((sound_node, tag_node, text_node)).parse(s) } /// A sound tag `[sound:resource]`, where `resource` is pointing to a sound or /// video file. -fn sound_node(s: &str) -> IResult { +fn sound_node(s: &str) -> IResult<'_, Node<'_>> { map( delimited(tag("[sound:"), is_not("]"), tag("]")), Node::SoundOrVideo, @@ -117,7 +117,7 @@ fn sound_node(s: &str) -> IResult { .parse(s) } -fn take_till_potential_tag_start(s: &str) -> IResult<&str> { +fn take_till_potential_tag_start(s: &str) -> IResult<'_, &str> { // first char could be '[', but wasn't part of a node, so skip (eof ends parse) let (after, offset) = anychar(s).map(|(s, c)| (s, c.len_utf8()))?; Ok(match after.find('[') { @@ -127,9 +127,9 @@ fn take_till_potential_tag_start(s: &str) -> IResult<&str> { } /// An Anki tag `[anki:tag...]...[/anki:tag]`. -fn tag_node(s: &str) -> IResult { +fn tag_node(s: &str) -> IResult<'_, Node<'_>> { /// Match the start of an opening tag and return its name. - fn name(s: &str) -> IResult<&str> { + fn name(s: &str) -> IResult<'_, &str> { preceded(tag("[anki:"), is_not("] \t\r\n")).parse(s) } @@ -139,12 +139,12 @@ fn tag_node(s: &str) -> IResult { ) -> impl FnMut(&'s str) -> IResult<'s, Vec<(&'s str, &'s str)>> + 'name { /// List of whitespace-separated `key=val` tuples, where `val` may be /// empty. - fn options(s: &str) -> IResult> { - fn key(s: &str) -> IResult<&str> { + fn options(s: &str) -> IResult<'_, Vec<(&str, &str)>> { + fn key(s: &str) -> IResult<'_, &str> { is_not("] \t\r\n=").parse(s) } - fn val(s: &str) -> IResult<&str> { + fn val(s: &str) -> IResult<'_, &str> { alt(( delimited(tag("\""), is_not0("\""), tag("\"")), is_not0("] \t\r\n\""), @@ -197,7 +197,7 @@ fn tag_node(s: &str) -> IResult { .parse(s) } -fn text_node(s: &str) -> IResult { +fn text_node(s: &str) -> IResult<'_, Node<'_>> { map(take_till_potential_tag_start, Node::Text).parse(s) } diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs index 02919dc12..70a5d1703 100644 --- a/rslib/src/cloze.rs +++ b/rslib/src/cloze.rs @@ -10,6 +10,7 @@ use std::sync::LazyLock; use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusion; use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionShape; use htmlescape::encode_attribute; +use itertools::Itertools; use nom::branch::alt; use nom::bytes::complete::tag; use nom::bytes::complete::take_while; @@ -26,7 +27,7 @@ use crate::template::RenderContext; use crate::text::strip_html_preserving_entities; static CLOZE: LazyLock = - LazyLock::new(|| Regex::new(r"(?s)\{\{c\d+::(.*?)(::.*?)?\}\}").unwrap()); + LazyLock::new(|| Regex::new(r"(?s)\{\{c[\d,]+::(.*?)(::.*?)?\}\}").unwrap()); static MATHJAX: LazyLock = LazyLock::new(|| { Regex::new( @@ -48,39 +49,42 @@ mod mathjax_caps { #[derive(Debug)] enum Token<'a> { // The parameter is the cloze number as is appears in the field content. - OpenCloze(u16), + OpenCloze(Vec), Text(&'a str), CloseCloze, } /// Tokenize string -fn tokenize(mut text: &str) -> impl Iterator { - fn open_cloze(text: &str) -> IResult<&str, Token> { +fn tokenize(mut text: &str) -> impl Iterator> { + fn open_cloze(text: &str) -> IResult<&str, Token<'_>> { // opening brackets and 'c' let (text, _opening_brackets_and_c) = tag("{{c")(text)?; - // following number - let (text, digits) = take_while(|c: char| c.is_ascii_digit())(text)?; - let digits: u16 = match digits.parse() { - Ok(digits) => digits, - Err(_) => { - // not a valid number; fail to recognize - return Err(nom::Err::Error(nom::error::make_error( - text, - nom::error::ErrorKind::Digit, - ))); - } - }; + // following comma-seperated numbers + let (text, ordinals) = take_while(|c: char| c.is_ascii_digit() || c == ',')(text)?; + let ordinals: Vec = ordinals + .split(',') + .filter_map(|s| s.parse().ok()) + .collect::>() // deduplicate + .into_iter() + .sorted() // set conversion can de-order + .collect(); + if ordinals.is_empty() { + return Err(nom::Err::Error(nom::error::make_error( + text, + nom::error::ErrorKind::Digit, + ))); + } // :: let (text, _colons) = tag("::")(text)?; - Ok((text, Token::OpenCloze(digits))) + Ok((text, Token::OpenCloze(ordinals))) } - fn close_cloze(text: &str) -> IResult<&str, Token> { + fn close_cloze(text: &str) -> IResult<&str, Token<'_>> { map(tag("}}"), |_| Token::CloseCloze).parse(text) } /// Match a run of text until an open/close marker is encountered. - fn normal_text(text: &str) -> IResult<&str, Token> { + fn normal_text(text: &str) -> IResult<&str, Token<'_>> { if text.is_empty() { return Err(nom::Err::Error(nom::error::make_error( text, @@ -121,18 +125,27 @@ enum TextOrCloze<'a> { #[derive(Debug)] struct ExtractedCloze<'a> { // `ordinal` is the cloze number as is appears in the field content. - ordinal: u16, + ordinals: Vec, nodes: Vec>, hint: Option<&'a str>, } +/// Generate a string representation of the ordinals for HTML +fn ordinals_str(ordinals: &[u16]) -> String { + ordinals + .iter() + .map(|o| o.to_string()) + .collect::>() + .join(",") +} + impl ExtractedCloze<'_> { /// Return the cloze's hint, or "..." if none was provided. fn hint(&self) -> &str { self.hint.unwrap_or("...") } - fn clozed_text(&self) -> Cow { + fn clozed_text(&self) -> Cow<'_, str> { // happy efficient path? if self.nodes.len() == 1 { if let TextOrCloze::Text(text) = self.nodes.last().unwrap() { @@ -151,6 +164,11 @@ impl ExtractedCloze<'_> { buf.into() } + /// Checks if this cloze is active for a given ordinal + fn contains_ordinal(&self, ordinal: u16) -> bool { + self.ordinals.contains(&ordinal) + } + /// If cloze starts with image-occlusion:, return the text following that. fn image_occlusion(&self) -> Option<&str> { let TextOrCloze::Text(text) = self.nodes.first()? else { @@ -165,10 +183,10 @@ fn parse_text_with_clozes(text: &str) -> Vec> { let mut output = vec![]; for token in tokenize(text) { match token { - Token::OpenCloze(ordinal) => { + Token::OpenCloze(ordinals) => { if open_clozes.len() < 10 { open_clozes.push(ExtractedCloze { - ordinal, + ordinals, nodes: Vec::with_capacity(1), // common case hint: None, }) @@ -214,7 +232,7 @@ fn reveal_cloze_text_in_nodes( output: &mut Vec, ) { if let TextOrCloze::Cloze(cloze) = node { - if cloze.ordinal == cloze_ord { + if cloze.contains_ordinal(cloze_ord) { if question { output.push(cloze.hint().into()) } else { @@ -234,14 +252,15 @@ fn reveal_cloze( active_cloze_found_in_text: &mut bool, buf: &mut String, ) { - let active = cloze.ordinal == cloze_ord; + let active = cloze.contains_ordinal(cloze_ord); *active_cloze_found_in_text |= active; + if let Some(image_occlusion_text) = cloze.image_occlusion() { buf.push_str(&render_image_occlusion( image_occlusion_text, question, active, - cloze.ordinal, + &cloze.ordinals, )); return; } @@ -265,7 +284,7 @@ fn reveal_cloze( buf, r#"[{}]"#, encode_attribute(&content_buf), - cloze.ordinal, + ordinals_str(&cloze.ordinals), cloze.hint() ) .unwrap(); @@ -274,7 +293,7 @@ fn reveal_cloze( write!( buf, r#""#, - cloze.ordinal + ordinals_str(&cloze.ordinals) ) .unwrap(); for node in &cloze.nodes { @@ -292,7 +311,7 @@ fn reveal_cloze( write!( buf, r#""#, - cloze.ordinal + ordinals_str(&cloze.ordinals) ) .unwrap(); for node in &cloze.nodes { @@ -308,23 +327,28 @@ fn reveal_cloze( } } -fn render_image_occlusion(text: &str, question_side: bool, active: bool, ordinal: u16) -> String { - if (question_side && active) || ordinal == 0 { +fn render_image_occlusion( + text: &str, + question_side: bool, + active: bool, + ordinals: &[u16], +) -> String { + if (question_side && active) || ordinals.contains(&0) { format!( r#"

"#, - ordinal, + ordinals_str(ordinals), &get_image_cloze_data(text) ) } else if !active { format!( r#"
"#, - ordinal, + ordinals_str(ordinals), &get_image_cloze_data(text) ) } else if !question_side && active { format!( r#"
"#, - ordinal, + ordinals_str(ordinals), &get_image_cloze_data(text) ) } else { @@ -338,7 +362,10 @@ pub fn parse_image_occlusions(text: &str) -> Vec { if let TextOrCloze::Cloze(cloze) = node { if cloze.image_occlusion().is_some() { if let Some(shape) = parse_image_cloze(cloze.image_occlusion().unwrap()) { - occlusions.entry(cloze.ordinal).or_default().push(shape); + // Associate this occlusion with all ordinals in this cloze + for &ordinal in &cloze.ordinals { + occlusions.entry(ordinal).or_default().push(shape.clone()); + } } } } @@ -353,7 +380,7 @@ pub fn parse_image_occlusions(text: &str) -> Vec { .collect() } -pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow { +pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<'_, str> { let mut buf = String::new(); let mut active_cloze_found_in_text = false; for node in &parse_text_with_clozes(text) { @@ -376,7 +403,7 @@ pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow } } -pub fn reveal_cloze_text_only(text: &str, cloze_ord: u16, question: bool) -> Cow { +pub fn reveal_cloze_text_only(text: &str, cloze_ord: u16, question: bool) -> Cow<'_, str> { let mut output = Vec::new(); for node in &parse_text_with_clozes(text) { reveal_cloze_text_in_nodes(node, cloze_ord, question, &mut output); @@ -384,7 +411,7 @@ pub fn reveal_cloze_text_only(text: &str, cloze_ord: u16, question: bool) -> Cow output.join(", ").into() } -pub fn extract_cloze_for_typing(text: &str, cloze_ord: u16) -> Cow { +pub fn extract_cloze_for_typing(text: &str, cloze_ord: u16) -> Cow<'_, str> { let mut output = Vec::new(); for node in &parse_text_with_clozes(text) { reveal_cloze_text_in_nodes(node, cloze_ord, false, &mut output); @@ -420,7 +447,7 @@ pub fn expand_clozes_to_reveal_latex(text: &str) -> String { pub(crate) fn contains_cloze(text: &str) -> bool { parse_text_with_clozes(text) .iter() - .any(|node| matches!(node, TextOrCloze::Cloze(e) if e.ordinal != 0)) + .any(|node| matches!(node, TextOrCloze::Cloze(e) if e.ordinals.iter().any(|&o| o != 0))) } /// Returns the set of cloze number as they appear in the fields's content. @@ -433,10 +460,12 @@ pub fn cloze_numbers_in_string(html: &str) -> HashSet { fn add_cloze_numbers_in_text_with_clozes(nodes: &[TextOrCloze], set: &mut HashSet) { for node in nodes { if let TextOrCloze::Cloze(cloze) = node { - if cloze.ordinal != 0 { - set.insert(cloze.ordinal); - add_cloze_numbers_in_text_with_clozes(&cloze.nodes, set); + for &ordinal in &cloze.ordinals { + if ordinal != 0 { + set.insert(ordinal); + } } + add_cloze_numbers_in_text_with_clozes(&cloze.nodes, set); } } } @@ -460,7 +489,7 @@ pub(crate) fn strip_clozes(text: &str) -> Cow<'_, str> { CLOZE.replace_all(text, "$1") } -fn strip_html_inside_mathjax(text: &str) -> Cow { +fn strip_html_inside_mathjax(text: &str) -> Cow<'_, str> { MATHJAX.replace_all(text, |caps: &Captures| -> String { format!( "{}{}{}", @@ -654,4 +683,160 @@ mod test { ) ); } + + #[test] + fn multi_card_card_generation() { + let text = "{{c1,2,3::multi}}"; + assert_eq!( + cloze_number_in_fields(vec![text]), + vec![1, 2, 3].into_iter().collect::>() + ); + } + + #[test] + fn multi_card_cloze_basic() { + let text = "{{c1,2::shared}} word and {{c1::first}} vs {{c2::second}}"; + + assert_eq!( + strip_html(&reveal_cloze_text(text, 1, true)).as_ref(), + "[...] word and [...] vs second" + ); + assert_eq!( + strip_html(&reveal_cloze_text(text, 2, true)).as_ref(), + "[...] word and first vs [...]" + ); + assert_eq!( + strip_html(&reveal_cloze_text(text, 1, false)).as_ref(), + "shared word and first vs second" + ); + assert_eq!( + strip_html(&reveal_cloze_text(text, 2, false)).as_ref(), + "shared word and first vs second" + ); + assert_eq!( + cloze_numbers_in_string(text), + vec![1, 2].into_iter().collect::>() + ); + } + + #[test] + fn multi_card_cloze_html_attributes() { + let text = "{{c1,2,3::multi}}"; + + let card1_html = reveal_cloze_text(text, 1, true); + assert!(card1_html.contains(r#"data-ordinal="1,2,3""#)); + + let card2_html = reveal_cloze_text(text, 2, true); + assert!(card2_html.contains(r#"data-ordinal="1,2,3""#)); + + let card3_html = reveal_cloze_text(text, 3, true); + assert!(card3_html.contains(r#"data-ordinal="1,2,3""#)); + } + + #[test] + fn multi_card_cloze_with_hints() { + let text = "{{c1,2::answer::hint}}"; + + assert_eq!( + strip_html(&reveal_cloze_text(text, 1, true)).as_ref(), + "[hint]" + ); + assert_eq!( + strip_html(&reveal_cloze_text(text, 2, true)).as_ref(), + "[hint]" + ); + + assert_eq!( + strip_html(&reveal_cloze_text(text, 1, false)).as_ref(), + "answer" + ); + assert_eq!( + strip_html(&reveal_cloze_text(text, 2, false)).as_ref(), + "answer" + ); + } + + #[test] + fn multi_card_cloze_edge_cases() { + assert_eq!( + cloze_numbers_in_string("{{c1,1,2::test}}"), + vec![1, 2].into_iter().collect::>() + ); + + assert_eq!( + cloze_numbers_in_string("{{c0,1,2::test}}"), + vec![1, 2].into_iter().collect::>() + ); + + assert_eq!( + cloze_numbers_in_string("{{c1,,3::test}}"), + vec![1, 3].into_iter().collect::>() + ); + } + + #[test] + fn multi_card_cloze_only_filter() { + let text = "{{c1,2::shared}} and {{c1::first}} vs {{c2::second}}"; + + assert_eq!(reveal_cloze_text_only(text, 1, true), "..., ..."); + assert_eq!(reveal_cloze_text_only(text, 2, true), "..., ..."); + assert_eq!(reveal_cloze_text_only(text, 1, false), "shared, first"); + assert_eq!(reveal_cloze_text_only(text, 2, false), "shared, second"); + } + + #[test] + fn multi_card_nested_cloze() { + let text = "{{c1,2::outer {{c3::inner}}}}"; + + assert_eq!( + strip_html(&reveal_cloze_text(text, 1, true)).as_ref(), + "[...]" + ); + + assert_eq!( + strip_html(&reveal_cloze_text(text, 2, true)).as_ref(), + "[...]" + ); + + assert_eq!( + strip_html(&reveal_cloze_text(text, 3, true)).as_ref(), + "outer [...]" + ); + + assert_eq!( + cloze_numbers_in_string(text), + vec![1, 2, 3].into_iter().collect::>() + ); + } + + #[test] + fn nested_parent_child_card_same_cloze() { + let text = "{{c1::outer {{c1::inner}}}}"; + + assert_eq!( + strip_html(&reveal_cloze_text(text, 1, true)).as_ref(), + "[...]" + ); + + assert_eq!( + cloze_numbers_in_string(text), + vec![1].into_iter().collect::>() + ); + } + + #[test] + fn multi_card_image_occlusion() { + let text = "{{c1,2::image-occlusion:rect:left=10:top=20:width=30:height=40}}"; + + let occlusions = parse_image_occlusions(text); + assert_eq!(occlusions.len(), 2); + assert!(occlusions.iter().any(|o| o.ordinal == 1)); + assert!(occlusions.iter().any(|o| o.ordinal == 2)); + + let card1_html = reveal_cloze_text(text, 1, true); + assert!(card1_html.contains(r#"data-ordinal="1,2""#)); + + let card2_html = reveal_cloze_text(text, 2, true); + assert!(card2_html.contains(r#"data-ordinal="1,2""#)); + } } diff --git a/rslib/src/collection/service.rs b/rslib/src/collection/service.rs index 2050a6897..a37360782 100644 --- a/rslib/src/collection/service.rs +++ b/rslib/src/collection/service.rs @@ -1,8 +1,10 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use anki_proto::collection::GetCustomColoursResponse; use anki_proto::generic; use crate::collection::Collection; +use crate::config::ConfigKey; use crate::error; use crate::prelude::BoolKey; use crate::prelude::Op; @@ -62,4 +64,13 @@ impl crate::services::CollectionService for Collection { }) .map(Into::into) } + + fn get_custom_colours( + &mut self, + ) -> error::Result { + let colours = self + .get_config_optional(ConfigKey::CustomColorPickerPalette) + .unwrap_or_default(); + Ok(GetCustomColoursResponse { colours }) + } } diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs index 5ece5b7e1..1e507281a 100644 --- a/rslib/src/config/mod.rs +++ b/rslib/src/config/mod.rs @@ -71,6 +71,7 @@ pub(crate) enum ConfigKey { NextNewCardPosition, #[strum(to_string = "schedVer")] SchedulerVersion, + CustomColorPickerPalette, } #[derive(PartialEq, Eq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] diff --git a/rslib/src/deckconfig/service.rs b/rslib/src/deckconfig/service.rs index 11c4288d3..b8987982b 100644 --- a/rslib/src/deckconfig/service.rs +++ b/rslib/src/deckconfig/service.rs @@ -115,7 +115,7 @@ impl crate::services::DeckConfigService for Collection { .storage .get_revlog_entries_for_searched_cards_in_card_order()?; - let config = guard.col.get_optimal_retention_parameters(revlogs)?; + let mut config = guard.col.get_optimal_retention_parameters(revlogs)?; let cards = guard .col .storage @@ -125,6 +125,8 @@ impl crate::services::DeckConfigService for Collection { .filter_map(|c| crate::card::Card::convert(c.clone(), days_elapsed, c.memory_state?)) .collect::>(); + config.deck_size = guard.cards; + let costs = (70u32..=99u32) .into_par_iter() .map(|dr| { diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index 0bd549a20..a9a27753e 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -216,9 +216,6 @@ impl Collection { for deck in self.storage.get_all_decks()? { if let Ok(normal) = deck.normal() { let deck_id = deck.id; - if let Some(desired_retention) = normal.desired_retention { - deck_desired_retention.insert(deck_id, desired_retention); - } // previous order & params let previous_config_id = DeckConfigId(normal.config_id); let previous_config = configs_before_update.get(&previous_config_id); @@ -226,21 +223,23 @@ impl Collection { .map(|c| c.inner.new_card_insert_order()) .unwrap_or_default(); let previous_params = previous_config.map(|c| c.fsrs_params()); - let previous_retention = previous_config.map(|c| c.inner.desired_retention); + let previous_preset_dr = previous_config.map(|c| c.inner.desired_retention); + let previous_deck_dr = normal.desired_retention; + let previous_dr = previous_deck_dr.or(previous_preset_dr); let previous_easy_days = previous_config.map(|c| &c.inner.easy_days_percentages); // if a selected (sub)deck, or its old config was removed, update deck to point // to new config - let current_config_id = if selected_deck_ids.contains(&deck.id) + let (current_config_id, current_deck_dr) = if selected_deck_ids.contains(&deck.id) || !configs_after_update.contains_key(&previous_config_id) { let mut updated = deck.clone(); updated.normal_mut()?.config_id = selected_config.id.0; update_deck_limits(updated.normal_mut()?, &req.limits, today); self.update_deck_inner(&mut updated, deck, usn)?; - selected_config.id + (selected_config.id, updated.normal()?.desired_retention) } else { - previous_config_id + (previous_config_id, previous_deck_dr) }; // if new order differs, deck needs re-sorting @@ -254,11 +253,12 @@ impl Collection { // if params differ, memory state needs to be recomputed let current_params = current_config.map(|c| c.fsrs_params()); - let current_retention = current_config.map(|c| c.inner.desired_retention); + let current_preset_dr = current_config.map(|c| c.inner.desired_retention); + let current_dr = current_deck_dr.or(current_preset_dr); let current_easy_days = current_config.map(|c| &c.inner.easy_days_percentages); if fsrs_toggled || previous_params != current_params - || previous_retention != current_retention + || previous_dr != current_dr || (req.fsrs_reschedule && previous_easy_days != current_easy_days) { decks_needing_memory_recompute @@ -266,7 +266,9 @@ impl Collection { .or_default() .push(deck_id); } - + if let Some(desired_retention) = current_deck_dr { + deck_desired_retention.insert(deck_id, desired_retention); + } self.adjust_remaining_steps_in_deck(deck_id, previous_config, current_config, usn)?; } } diff --git a/rslib/src/decks/name.rs b/rslib/src/decks/name.rs index 09fd2fe65..c7e79a782 100644 --- a/rslib/src/decks/name.rs +++ b/rslib/src/decks/name.rs @@ -191,7 +191,7 @@ fn invalid_char_for_deck_component(c: char) -> bool { c.is_ascii_control() } -fn normalized_deck_name_component(comp: &str) -> Cow { +fn normalized_deck_name_component(comp: &str) -> Cow<'_, str> { let mut out = normalize_to_nfc(comp); if out.contains(invalid_char_for_deck_component) { out = out.replace(invalid_char_for_deck_component, "").into(); diff --git a/rslib/src/decks/schema11.rs b/rslib/src/decks/schema11.rs index 5cd4094f0..3d4e30b96 100644 --- a/rslib/src/decks/schema11.rs +++ b/rslib/src/decks/schema11.rs @@ -135,6 +135,8 @@ pub struct NormalDeckSchema11 { review_limit_today: Option, #[serde(default, deserialize_with = "default_on_invalid")] new_limit_today: Option, + #[serde(default, deserialize_with = "default_on_invalid")] + desired_retention: Option, } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] @@ -249,6 +251,7 @@ impl Default for NormalDeckSchema11 { new_limit: None, review_limit_today: None, new_limit_today: None, + desired_retention: None, } } } @@ -325,7 +328,7 @@ impl From for NormalDeck { new_limit: deck.new_limit, review_limit_today: deck.review_limit_today, new_limit_today: deck.new_limit_today, - desired_retention: None, + desired_retention: deck.desired_retention.map(|v| v as f32 / 100.0), } } } @@ -367,6 +370,7 @@ impl From for DeckSchema11 { new_limit: norm.new_limit, review_limit_today: norm.review_limit_today, new_limit_today: norm.new_limit_today, + desired_retention: norm.desired_retention.map(|v| (v * 100.0) as u32), common: deck.into(), }), DeckKind::Filtered(ref filt) => DeckSchema11::Filtered(FilteredDeckSchema11 { @@ -431,7 +435,8 @@ static RESERVED_DECK_KEYS: Set<&'static str> = phf_set! { "browserCollapsed", "extendRev", "id", - "collapsed" + "collapsed", + "desiredRetention", }; impl From<&Deck> for DeckTodaySchema11 { diff --git a/rslib/src/import_export/gather.rs b/rslib/src/import_export/gather.rs index 99e4babe2..7249e134a 100644 --- a/rslib/src/import_export/gather.rs +++ b/rslib/src/import_export/gather.rs @@ -231,7 +231,10 @@ fn svg_getter(notetypes: &[Notetype]) -> impl Fn(NotetypeId) -> bool { } impl Collection { - fn gather_notes(&mut self, search: impl TryIntoSearch) -> Result<(Vec, NoteTableGuard)> { + fn gather_notes( + &mut self, + search: impl TryIntoSearch, + ) -> Result<(Vec, NoteTableGuard<'_>)> { let guard = self.search_notes_into_table(search)?; guard .col @@ -240,7 +243,7 @@ impl Collection { .map(|notes| (notes, guard)) } - fn gather_cards(&mut self) -> Result<(Vec, CardTableGuard)> { + fn gather_cards(&mut self) -> Result<(Vec, CardTableGuard<'_>)> { let guard = self.search_cards_of_notes_into_table()?; guard .col diff --git a/rslib/src/import_export/package/apkg/import/notes.rs b/rslib/src/import_export/package/apkg/import/notes.rs index ba5178a18..ce4266289 100644 --- a/rslib/src/import_export/package/apkg/import/notes.rs +++ b/rslib/src/import_export/package/apkg/import/notes.rs @@ -664,7 +664,7 @@ mod test { self } - fn import(self, col: &mut Collection) -> NoteContext { + fn import(self, col: &mut Collection) -> NoteContext<'_> { let mut progress_handler = col.new_progress_handler(); let media_map = Box::leak(Box::new(self.media_map)); let mut ctx = NoteContext::new( diff --git a/rslib/src/import_export/package/media.rs b/rslib/src/import_export/package/media.rs index ff5bdf4d7..8a7e5b726 100644 --- a/rslib/src/import_export/package/media.rs +++ b/rslib/src/import_export/package/media.rs @@ -154,7 +154,7 @@ pub(super) fn extract_media_entries( } } -pub(super) fn safe_normalized_file_name(name: &str) -> Result> { +pub(super) fn safe_normalized_file_name(name: &str) -> Result> { if !filename_is_safe(name) { Err(AnkiError::ImportError { source: ImportError::Corrupt, diff --git a/rslib/src/import_export/text/csv/export.rs b/rslib/src/import_export/text/csv/export.rs index 885035b7e..1af1cfaa9 100644 --- a/rslib/src/import_export/text/csv/export.rs +++ b/rslib/src/import_export/text/csv/export.rs @@ -147,7 +147,7 @@ fn rendered_nodes_to_str(nodes: &[RenderedNode]) -> String { .join("") } -fn field_to_record_field(field: &str, with_html: bool) -> Cow { +fn field_to_record_field(field: &str, with_html: bool) -> Cow<'_, str> { let mut text = strip_redundant_sections(field); if !with_html { text = text.map_cow(|t| html_to_text_line(t, false)); @@ -155,7 +155,7 @@ fn field_to_record_field(field: &str, with_html: bool) -> Cow { text } -fn strip_redundant_sections(text: &str) -> Cow { +fn strip_redundant_sections(text: &str) -> Cow<'_, str> { static RE: LazyLock = LazyLock::new(|| { Regex::new( r"(?isx) @@ -169,7 +169,7 @@ fn strip_redundant_sections(text: &str) -> Cow { RE.replace_all(text.as_ref(), "") } -fn strip_answer_side_question(text: &str) -> Cow { +fn strip_answer_side_question(text: &str) -> Cow<'_, str> { static RE: LazyLock = LazyLock::new(|| Regex::new(r"(?is)^.*
\n*").unwrap()); RE.replace_all(text.as_ref(), "") @@ -251,7 +251,7 @@ impl NoteContext { .chain(self.tags(note)) } - fn notetype_name(&self, note: &Note) -> Option> { + fn notetype_name(&self, note: &Note) -> Option> { self.with_notetype.then(|| { self.notetypes .get(¬e.notetype_id) @@ -259,7 +259,7 @@ impl NoteContext { }) } - fn deck_name(&self, note: &Note) -> Option> { + fn deck_name(&self, note: &Note) -> Option> { self.with_deck.then(|| { self.deck_ids .get(¬e.id) @@ -268,7 +268,7 @@ impl NoteContext { }) } - fn tags(&self, note: &Note) -> Option> { + fn tags(&self, note: &Note) -> Option> { self.with_tags .then(|| Cow::from(note.tags.join(" ").into_bytes())) } diff --git a/rslib/src/import_export/text/import.rs b/rslib/src/import_export/text/import.rs index 202189eb6..1e6f85b3f 100644 --- a/rslib/src/import_export/text/import.rs +++ b/rslib/src/import_export/text/import.rs @@ -511,7 +511,7 @@ impl NoteContext<'_> { } impl Note { - fn first_field_stripped(&self) -> Cow { + fn first_field_stripped(&self) -> Cow<'_, str> { strip_html_preserving_media_filenames(&self.fields()[0]) } } @@ -623,7 +623,7 @@ impl ForeignNote { .all(|(opt, field)| opt.as_ref().map(|s| s == field).unwrap_or(true)) } - fn first_field_stripped(&self) -> Option> { + fn first_field_stripped(&self) -> Option> { self.fields .first() .and_then(|s| s.as_ref()) diff --git a/rslib/src/latex.rs b/rslib/src/latex.rs index e5cb002ac..02056b721 100644 --- a/rslib/src/latex.rs +++ b/rslib/src/latex.rs @@ -48,7 +48,7 @@ pub struct ExtractedLatex { pub(crate) fn extract_latex_expanding_clozes( text: &str, svg: bool, -) -> (Cow, Vec) { +) -> (Cow<'_, str>, Vec) { if text.contains("{{c") { let expanded = expand_clozes_to_reveal_latex(text); let (text, extracts) = extract_latex(&expanded, svg); @@ -60,7 +60,7 @@ pub(crate) fn extract_latex_expanding_clozes( /// Extract LaTeX from the provided text. /// Expects cloze deletions to already be expanded. -pub fn extract_latex(text: &str, svg: bool) -> (Cow, Vec) { +pub fn extract_latex(text: &str, svg: bool) -> (Cow<'_, str>, Vec) { let mut extracted = vec![]; let new_text = LATEX.replace_all(text, |caps: &Captures| { @@ -84,7 +84,7 @@ pub fn extract_latex(text: &str, svg: bool) -> (Cow, Vec) { (new_text, extracted) } -fn strip_html_for_latex(html: &str) -> Cow { +fn strip_html_for_latex(html: &str) -> Cow<'_, str> { let mut out: Cow = html.into(); if let Cow::Owned(o) = LATEX_NEWLINES.replace_all(html, "\n") { out = o.into(); diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs index 6974e2f81..ce17b40bb 100644 --- a/rslib/src/media/files.rs +++ b/rslib/src/media/files.rs @@ -91,7 +91,7 @@ fn nonbreaking_space(char: char) -> bool { /// - Any problem characters are removed. /// - Windows device names like CON and PRN have '_' appended /// - The filename is limited to 120 bytes. -pub(crate) fn normalize_filename(fname: &str) -> Cow { +pub(crate) fn normalize_filename(fname: &str) -> Cow<'_, str> { let mut output = Cow::Borrowed(fname); if !is_nfc(output.as_ref()) { @@ -102,7 +102,7 @@ pub(crate) fn normalize_filename(fname: &str) -> Cow { } /// See normalize_filename(). This function expects NFC-normalized input. -pub(crate) fn normalize_nfc_filename(mut fname: Cow) -> Cow { +pub(crate) fn normalize_nfc_filename(mut fname: Cow<'_, str>) -> Cow<'_, str> { if fname.contains(disallowed_char) { fname = fname.replace(disallowed_char, "").into() } @@ -137,7 +137,7 @@ pub(crate) fn normalize_nfc_filename(mut fname: Cow) -> Cow { /// but can be accessed as NFC. On these devices, if the filename /// is otherwise valid, the filename is returned as NFC. #[allow(clippy::collapsible_else_if)] -pub(crate) fn filename_if_normalized(fname: &str) -> Option> { +pub(crate) fn filename_if_normalized(fname: &str) -> Option> { if cfg!(target_vendor = "apple") { if !is_nfc(fname) { let as_nfc = fname.chars().nfc().collect::(); @@ -208,7 +208,7 @@ pub(crate) fn add_hash_suffix_to_file_stem(fname: &str, hash: &Sha1Hash) -> Stri } /// If filename is longer than max_bytes, truncate it. -fn truncate_filename(fname: &str, max_bytes: usize) -> Cow { +fn truncate_filename(fname: &str, max_bytes: usize) -> Cow<'_, str> { if fname.len() <= max_bytes { return Cow::Borrowed(fname); } diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs index 932022e99..2b5ea2921 100644 --- a/rslib/src/notes/mod.rs +++ b/rslib/src/notes/mod.rs @@ -87,7 +87,7 @@ impl TryFrom for AddNoteRequest { } impl Collection { - pub fn add_note(&mut self, note: &mut Note, did: DeckId) -> Result> { + pub fn add_note(&mut self, note: &mut Note, did: DeckId) -> Result> { self.transact(Op::AddNote, |col| col.add_note_inner(note, did)) } @@ -372,7 +372,7 @@ impl Collection { Ok(()) } - pub(crate) fn add_note_inner(&mut self, note: &mut Note, did: DeckId) -> Result<()> { + pub(crate) fn add_note_inner(&mut self, note: &mut Note, did: DeckId) -> Result { let nt = self .get_notetype(note.notetype_id)? .or_invalid("missing note type")?; @@ -383,10 +383,11 @@ impl Collection { note.prepare_for_update(ctx.notetype, normalize_text)?; note.set_modified(ctx.usn); self.add_note_only_undoable(note)?; - self.generate_cards_for_new_note(&ctx, note, did)?; + let count = self.generate_cards_for_new_note(&ctx, note, did)?; self.set_last_deck_for_notetype(note.notetype_id, did)?; self.set_last_notetype_for_deck(did, note.notetype_id)?; - self.set_current_notetype_id(note.notetype_id) + self.set_current_notetype_id(note.notetype_id)?; + Ok(count) } pub fn update_note(&mut self, note: &mut Note) -> Result> { diff --git a/rslib/src/notetype/cardgen.rs b/rslib/src/notetype/cardgen.rs index 8e03d8ee4..b2a100054 100644 --- a/rslib/src/notetype/cardgen.rs +++ b/rslib/src/notetype/cardgen.rs @@ -215,7 +215,7 @@ impl Collection { ctx: &CardGenContext>, note: &Note, target_deck_id: DeckId, - ) -> Result<()> { + ) -> Result { self.generate_cards_for_note( ctx, note, @@ -231,7 +231,8 @@ impl Collection { note: &Note, ) -> Result<()> { let existing = self.storage.existing_cards_for_note(note.id)?; - self.generate_cards_for_note(ctx, note, &existing, ctx.last_deck, &mut Default::default()) + self.generate_cards_for_note(ctx, note, &existing, ctx.last_deck, &mut Default::default())?; + Ok(()) } fn generate_cards_for_note( @@ -241,12 +242,13 @@ impl Collection { existing: &[AlreadyGeneratedCardInfo], target_deck_id: Option, cache: &mut CardGenCache, - ) -> Result<()> { + ) -> Result { let cards = ctx.new_cards_required(note, existing, true); if cards.is_empty() { - return Ok(()); + return Ok(0); } - self.add_generated_cards(note.id, &cards, target_deck_id, cache) + self.add_generated_cards(note.id, &cards, target_deck_id, cache)?; + Ok(cards.len()) } pub(crate) fn generate_cards_for_notetype( diff --git a/rslib/src/notetype/render.rs b/rslib/src/notetype/render.rs index 08c5677b0..19f5208dc 100644 --- a/rslib/src/notetype/render.rs +++ b/rslib/src/notetype/render.rs @@ -25,7 +25,7 @@ pub struct RenderCardOutput { impl RenderCardOutput { /// The question text. This is only valid to call when partial_render=false. - pub fn question(&self) -> Cow { + pub fn question(&self) -> Cow<'_, str> { match self.qnodes.as_slice() { [RenderedNode::Text { text }] => text.into(), _ => "not fully rendered".into(), @@ -33,7 +33,7 @@ impl RenderCardOutput { } /// The answer text. This is only valid to call when partial_render=false. - pub fn answer(&self) -> Cow { + pub fn answer(&self) -> Cow<'_, str> { match self.anodes.as_slice() { [RenderedNode::Text { text }] => text.into(), _ => "not fully rendered".into(), diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs index 9b5df66d5..b27881809 100644 --- a/rslib/src/notetype/stock.rs +++ b/rslib/src/notetype/stock.rs @@ -122,7 +122,7 @@ pub(crate) fn basic(tr: &I18n) -> Notetype { pub(crate) fn basic_typing(tr: &I18n) -> Notetype { let mut nt = basic(tr); - nt.config.original_stock_kind = StockKind::BasicTyping as i32; + nt.config.original_stock_kind = OriginalStockKind::BasicTyping as i32; nt.name = tr.notetypes_basic_type_answer_name().into(); let front = tr.notetypes_front_field(); let back = tr.notetypes_back_field(); @@ -138,7 +138,7 @@ pub(crate) fn basic_typing(tr: &I18n) -> Notetype { pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype { let mut nt = basic(tr); - nt.config.original_stock_kind = StockKind::BasicAndReversed as i32; + nt.config.original_stock_kind = OriginalStockKind::BasicAndReversed as i32; nt.name = tr.notetypes_basic_reversed_name().into(); let front = tr.notetypes_front_field(); let back = tr.notetypes_back_field(); @@ -156,7 +156,7 @@ pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype { pub(crate) fn basic_optional_reverse(tr: &I18n) -> Notetype { let mut nt = basic_forward_reverse(tr); - nt.config.original_stock_kind = StockKind::BasicOptionalReversed as i32; + nt.config.original_stock_kind = OriginalStockKind::BasicOptionalReversed as i32; nt.name = tr.notetypes_basic_optional_reversed_name().into(); let addrev = tr.notetypes_add_reverse_field(); nt.add_field(addrev.as_ref()); diff --git a/rslib/src/revlog/mod.rs b/rslib/src/revlog/mod.rs index f52698388..fbb9b459a 100644 --- a/rslib/src/revlog/mod.rs +++ b/rslib/src/revlog/mod.rs @@ -85,6 +85,15 @@ impl RevlogEntry { .unwrap() } + pub(crate) fn last_interval_secs(&self) -> u32 { + u32::try_from(if self.last_interval > 0 { + self.last_interval.saturating_mul(86_400) + } else { + self.last_interval.saturating_mul(-1) + }) + .unwrap() + } + /// Returns true if this entry represents a reset operation. /// These entries are created when a card is reset using /// [`Collection::reschedule_cards_as_new`]. diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 6ff8c6e2d..a71c6330f 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -443,9 +443,20 @@ impl Collection { .storage .get_deck(card.deck_id)? .or_not_found(card.deck_id)?; - let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; + let home_deck = if card.original_deck_id.0 == 0 { + &deck + } else { + &self + .storage + .get_deck(card.original_deck_id)? + .or_not_found(card.original_deck_id)? + }; + let config = self + .storage + .get_deck_config(home_deck.config_id().or_invalid("home deck is filtered")?)? + .unwrap_or_default(); - let desired_retention = deck.effective_desired_retention(&config); + let desired_retention = home_deck.effective_desired_retention(&config); let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs); let fsrs_next_states = if fsrs_enabled { let params = config.fsrs_params(); diff --git a/rslib/src/scheduler/fsrs/error.rs b/rslib/src/scheduler/fsrs/error.rs index d5b596a36..404ee3605 100644 --- a/rslib/src/scheduler/fsrs/error.rs +++ b/rslib/src/scheduler/fsrs/error.rs @@ -13,13 +13,7 @@ impl From for AnkiError { FSRSError::OptimalNotFound => AnkiError::FsrsUnableToDetermineDesiredRetention, FSRSError::Interrupted => AnkiError::Interrupted, FSRSError::InvalidParameters => AnkiError::FsrsParamsInvalid, - FSRSError::InvalidInput => AnkiError::InvalidInput { - source: InvalidInputError { - message: "invalid params provided".to_string(), - source: None, - backtrace: None, - }, - }, + FSRSError::InvalidInput => AnkiError::FsrsParamsInvalid, FSRSError::InvalidDeckSize => AnkiError::InvalidInput { source: InvalidInputError { message: "no cards to simulate".to_string(), diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 420ead5a3..303bbfd91 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -136,6 +136,19 @@ impl Collection { let deckconfig_id = deck.config_id().unwrap(); // reschedule it let original_interval = card.interval; + let min_interval = |interval: u32| { + let previous_interval = + last_info.previous_interval.unwrap_or(0); + if interval > previous_interval { + // interval grew; don't allow fuzzed interval to + // be less than previous+1 + previous_interval + 1 + } else { + // interval shrunk; don't restrict negative fuzz + 0 + } + .max(1) + }; let interval = fsrs.next_interval( Some(state.stability), desired_retention, @@ -146,7 +159,7 @@ impl Collection { .and_then(|r| { r.find_interval( interval, - 1, + min_interval(interval as u32), req.max_interval, days_elapsed as u32, deckconfig_id, @@ -157,7 +170,7 @@ impl Collection { with_review_fuzz( card.get_fuzz_factor(true), interval, - 1, + min_interval(interval as u32), req.max_interval, ) }); @@ -310,6 +323,9 @@ pub(crate) struct LastRevlogInfo { /// reviewed the card and now, so that we can determine an accurate period /// when the card has subsequently been rescheduled to a different day. pub(crate) last_reviewed_at: Option, + /// The interval before the latest review. Used to prevent fuzz from going + /// backwards when rescheduling the card + pub(crate) previous_interval: Option, } /// Return a map of cards to info about last review. @@ -321,14 +337,27 @@ pub(crate) fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap= 0 && e.button_chosen > 1 { + Some(e.last_interval as u32) + } else { + None + }; } else if e.is_reset() { last_reviewed_at = None; + previous_interval = None; } } - out.insert(card_id, LastRevlogInfo { last_reviewed_at }); + out.insert( + card_id, + LastRevlogInfo { + last_reviewed_at, + previous_interval, + }, + ); }); out } diff --git a/rslib/src/scheduler/fsrs/params.rs b/rslib/src/scheduler/fsrs/params.rs index 726870fe1..1fb1d58b8 100644 --- a/rslib/src/scheduler/fsrs/params.rs +++ b/rslib/src/scheduler/fsrs/params.rs @@ -174,7 +174,7 @@ impl Collection { } } - let health_check_passed = if health_check { + let health_check_passed = if health_check && input.train_set.len() > 300 { let fsrs = FSRS::new(None)?; fsrs.evaluate_with_time_series_splits(input, |_| true) .ok() @@ -478,27 +478,42 @@ pub(crate) fn reviews_for_fsrs( })) .collect_vec(); - let skip = if training { 1 } else { 0 }; - // Convert the remaining entries into separate FSRSItems, where each item - // contains all reviews done until then. - let items: Vec<(RevlogId, FSRSItem)> = entries - .iter() - .enumerate() - .skip(skip) - .map(|(outer_idx, entry)| { - let reviews = entries - .iter() - .take(outer_idx + 1) - .enumerate() - .map(|(inner_idx, r)| FSRSReview { - rating: r.button_chosen as u32, - delta_t: delta_ts[inner_idx], - }) - .collect(); - (entry.id, FSRSItem { reviews }) - }) - .filter(|(_, item)| !training || item.reviews.last().unwrap().delta_t > 0) - .collect_vec(); + let items = if training { + // Convert the remaining entries into separate FSRSItems, where each item + // contains all reviews done until then. + let mut items = Vec::with_capacity(entries.len()); + let mut current_reviews = Vec::with_capacity(entries.len()); + for (idx, (entry, &delta_t)) in entries.iter().zip(delta_ts.iter()).enumerate() { + current_reviews.push(FSRSReview { + rating: entry.button_chosen as u32, + delta_t, + }); + if idx >= 1 && delta_t > 0 { + items.push(( + entry.id, + FSRSItem { + reviews: current_reviews.clone(), + }, + )); + } + } + items + } else { + // When not training, we only need the final FSRS item, which represents + // the complete history of the card. This avoids expensive clones in a loop. + let reviews = entries + .iter() + .zip(delta_ts.iter()) + .map(|(entry, &delta_t)| FSRSReview { + rating: entry.button_chosen as u32, + delta_t, + }) + .collect(); + let last_entry = entries.last().unwrap(); + + vec![(last_entry.id, FSRSItem { reviews })] + }; + if items.is_empty() { None } else { @@ -738,7 +753,7 @@ pub(crate) mod tests { ], false, ), - fsrs_items!([review(0)], [review(0), review(1)]) + fsrs_items!([review(0), review(1)]) ); } @@ -809,7 +824,7 @@ pub(crate) mod tests { // R | A X R assert_eq!( convert_ignore_before(revlogs, false, days_ago_ms(9)), - fsrs_items!([review(0)], [review(0), review(2)]) + fsrs_items!([review(0), review(2)]) ); } @@ -828,6 +843,9 @@ pub(crate) mod tests { assert_eq!( convert_ignore_before(revlogs, false, days_ago_ms(9)) .unwrap() + .last() + .unwrap() + .reviews .len(), 2 ); @@ -849,6 +867,9 @@ pub(crate) mod tests { assert_eq!( convert_ignore_before(revlogs, false, days_ago_ms(9)) .unwrap() + .last() + .unwrap() + .reviews .len(), 2 ); diff --git a/rslib/src/scheduler/fsrs/rescheduler.rs b/rslib/src/scheduler/fsrs/rescheduler.rs index db490b3e4..37c824230 100644 --- a/rslib/src/scheduler/fsrs/rescheduler.rs +++ b/rslib/src/scheduler/fsrs/rescheduler.rs @@ -115,13 +115,14 @@ impl Rescheduler { pub fn find_interval( &self, interval: f32, - minimum: u32, - maximum: u32, + minimum_interval: u32, + maximum_interval: u32, days_elapsed: u32, deckconfig_id: DeckConfigId, fuzz_seed: Option, ) -> Option { - let (before_days, after_days) = constrained_fuzz_bounds(interval, minimum, maximum); + let (before_days, after_days) = + constrained_fuzz_bounds(interval, minimum_interval, maximum_interval); // Don't reschedule the card when it's overdue if after_days < days_elapsed { diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 62130c4d0..58c5fd5a0 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -97,7 +97,7 @@ fn create_review_priority_fn( // Interval-based ordering IntervalsAscending => wrap!(|c, _w| c.interval as i32), - IntervalsDescending => wrap!(|c, _w| -(c.interval as i32)), + IntervalsDescending => wrap!(|c, _w| (c.interval as i32).saturating_neg()), // Retrievability-based ordering RetrievabilityAscending => { wrap!(move |c, w| (c.retrievability(w) * 1000.0) as i32) diff --git a/rslib/src/scheduler/queue/builder/gathering.rs b/rslib/src/scheduler/queue/builder/gathering.rs index fb6274de5..293b50dc4 100644 --- a/rslib/src/scheduler/queue/builder/gathering.rs +++ b/rslib/src/scheduler/queue/builder/gathering.rs @@ -61,28 +61,26 @@ impl QueueBuilder { } fn gather_new_cards(&mut self, col: &mut Collection) -> Result<()> { + let salt = Self::knuth_salt(self.context.timing.days_elapsed); match self.context.sort_options.new_gather_priority { NewCardGatherPriority::Deck => { self.gather_new_cards_by_deck(col, NewCardSorting::LowestPosition) } - NewCardGatherPriority::DeckThenRandomNotes => self.gather_new_cards_by_deck( - col, - NewCardSorting::RandomNotes(self.context.timing.days_elapsed), - ), + NewCardGatherPriority::DeckThenRandomNotes => { + self.gather_new_cards_by_deck(col, NewCardSorting::RandomNotes(salt)) + } NewCardGatherPriority::LowestPosition => { self.gather_new_cards_sorted(col, NewCardSorting::LowestPosition) } NewCardGatherPriority::HighestPosition => { self.gather_new_cards_sorted(col, NewCardSorting::HighestPosition) } - NewCardGatherPriority::RandomNotes => self.gather_new_cards_sorted( - col, - NewCardSorting::RandomNotes(self.context.timing.days_elapsed), - ), - NewCardGatherPriority::RandomCards => self.gather_new_cards_sorted( - col, - NewCardSorting::RandomCards(self.context.timing.days_elapsed), - ), + NewCardGatherPriority::RandomNotes => { + self.gather_new_cards_sorted(col, NewCardSorting::RandomNotes(salt)) + } + NewCardGatherPriority::RandomCards => { + self.gather_new_cards_sorted(col, NewCardSorting::RandomCards(salt)) + } } } @@ -169,4 +167,10 @@ impl QueueBuilder { true } } + + // Generates a salt for use with fnvhash. Useful to increase randomness + // when the base salt is a small integer. + fn knuth_salt(base_salt: u32) -> u32 { + base_salt.wrapping_mul(2654435761) + } } diff --git a/rslib/src/scheduler/states/load_balancer.rs b/rslib/src/scheduler/states/load_balancer.rs index 20b6936df..8cb9e6a1c 100644 --- a/rslib/src/scheduler/states/load_balancer.rs +++ b/rslib/src/scheduler/states/load_balancer.rs @@ -174,7 +174,7 @@ impl LoadBalancer { &self, note_id: Option, deckconfig_id: DeckConfigId, - ) -> LoadBalancerContext { + ) -> LoadBalancerContext<'_> { LoadBalancerContext { load_balancer: self, note_id, diff --git a/rslib/src/search/builder.rs b/rslib/src/search/builder.rs index a76af0560..0c22ff1eb 100644 --- a/rslib/src/search/builder.rs +++ b/rslib/src/search/builder.rs @@ -6,6 +6,7 @@ use std::mem; use itertools::Itertools; use super::writer::write_nodes; +use super::FieldSearchMode; use super::Node; use super::SearchNode; use super::StateKind; @@ -174,7 +175,7 @@ impl SearchNode { pub fn from_tag_name(name: &str) -> Self { Self::Tag { tag: escape_anki_wildcards_for_search_node(name), - is_re: false, + mode: FieldSearchMode::Normal, } } diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index d42ea8323..0dd52dbc3 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -13,6 +13,7 @@ pub use builder::JoinSearches; pub use builder::Negated; pub use builder::SearchBuilder; pub use parser::parse as parse_search; +pub use parser::FieldSearchMode; pub use parser::Node; pub use parser::PropertyKind; pub use parser::RatingKind; @@ -226,7 +227,7 @@ impl Collection { &mut self, search: impl TryIntoSearch, mode: SortMode, - ) -> Result { + ) -> Result> { let top_node = search.try_into_search()?; let writer = SqlWriter::new(self, ReturnItemType::Cards); let want_order = mode != SortMode::NoOrder; @@ -299,7 +300,7 @@ impl Collection { pub(crate) fn search_notes_into_table( &mut self, search: impl TryIntoSearch, - ) -> Result { + ) -> Result> { let top_node = search.try_into_search()?; let writer = SqlWriter::new(self, ReturnItemType::Notes); let mode = SortMode::NoOrder; @@ -320,7 +321,7 @@ impl Collection { /// Place the ids of cards with notes in 'search_nids' into 'search_cids'. /// Returns number of added cards. - pub(crate) fn search_cards_of_notes_into_table(&mut self) -> Result { + pub(crate) fn search_cards_of_notes_into_table(&mut self) -> Result> { self.storage.setup_searched_cards_table()?; let cards = self.storage.search_cards_of_notes_into_table()?; Ok(CardTableGuard { cards, col: self }) diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index ae166ef54..5928bf486 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -3,6 +3,7 @@ use std::sync::LazyLock; +use anki_proto::search::search_node::FieldSearchMode as FieldSearchModeProto; use nom::branch::alt; use nom::bytes::complete::escaped; use nom::bytes::complete::is_not; @@ -27,7 +28,6 @@ use crate::error::ParseError; use crate::error::Result; use crate::error::SearchErrorKind as FailKind; use crate::prelude::*; - type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err>>; type ParseResult<'a, O> = std::result::Result>>; @@ -48,6 +48,23 @@ pub enum Node { Search(SearchNode), } +#[derive(Copy, Debug, PartialEq, Eq, Clone)] +pub enum FieldSearchMode { + Normal, + Regex, + NoCombining, +} + +impl From for FieldSearchMode { + fn from(mode: FieldSearchModeProto) -> Self { + match mode { + FieldSearchModeProto::Normal => Self::Normal, + FieldSearchModeProto::Regex => Self::Regex, + FieldSearchModeProto::Nocombining => Self::NoCombining, + } + } +} + #[derive(Debug, PartialEq, Clone)] pub enum SearchNode { // text without a colon @@ -56,7 +73,7 @@ pub enum SearchNode { SingleField { field: String, text: String, - is_re: bool, + mode: FieldSearchMode, }, AddedInDays(u32), EditedInDays(u32), @@ -77,7 +94,7 @@ pub enum SearchNode { }, Tag { tag: String, - is_re: bool, + mode: FieldSearchMode, }, Duplicates { notetype_id: NotetypeId, @@ -158,7 +175,7 @@ pub fn parse(input: &str) -> Result> { /// Zero or more nodes inside brackets, eg 'one OR two -three'. /// Empty vec must be handled by caller. -fn group_inner(input: &str) -> IResult> { +fn group_inner(input: &str) -> IResult<'_, Vec> { let mut remaining = input; let mut nodes = vec![]; @@ -203,16 +220,16 @@ fn group_inner(input: &str) -> IResult> { Ok((remaining, nodes)) } -fn whitespace0(s: &str) -> IResult> { +fn whitespace0(s: &str) -> IResult<'_, Vec> { many0(one_of(" \u{3000}")).parse(s) } /// Optional leading space, then a (negated) group or text -fn node(s: &str) -> IResult { +fn node(s: &str) -> IResult<'_, Node> { preceded(whitespace0, alt((negated_node, group, text))).parse(s) } -fn negated_node(s: &str) -> IResult { +fn negated_node(s: &str) -> IResult<'_, Node> { map(preceded(char('-'), alt((group, text))), |node| { Node::Not(Box::new(node)) }) @@ -220,7 +237,7 @@ fn negated_node(s: &str) -> IResult { } /// One or more nodes surrounded by brackets, eg (one OR two) -fn group(s: &str) -> IResult { +fn group(s: &str) -> IResult<'_, Node> { let (opened, _) = char('(')(s)?; let (tail, inner) = group_inner(opened)?; if let Some(remaining) = tail.strip_prefix(')') { @@ -235,18 +252,18 @@ fn group(s: &str) -> IResult { } /// Either quoted or unquoted text -fn text(s: &str) -> IResult { +fn text(s: &str) -> IResult<'_, Node> { alt((quoted_term, partially_quoted_term, unquoted_term)).parse(s) } /// Quoted text, including the outer double quotes. -fn quoted_term(s: &str) -> IResult { +fn quoted_term(s: &str) -> IResult<'_, Node> { let (remaining, term) = quoted_term_str(s)?; Ok((remaining, Node::Search(search_node_for_text(term)?))) } /// eg deck:"foo bar" - quotes must come after the : -fn partially_quoted_term(s: &str) -> IResult { +fn partially_quoted_term(s: &str) -> IResult<'_, Node> { let (remaining, (key, val)) = separated_pair( escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(" \u{3000}")), char(':'), @@ -260,7 +277,7 @@ fn partially_quoted_term(s: &str) -> IResult { } /// Unquoted text, terminated by whitespace or unescaped ", ( or ) -fn unquoted_term(s: &str) -> IResult { +fn unquoted_term(s: &str) -> IResult<'_, Node> { match escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}"))(s) { Ok((tail, term)) => { if term.is_empty() { @@ -297,7 +314,7 @@ fn unquoted_term(s: &str) -> IResult { } /// Non-empty string delimited by unescaped double quotes. -fn quoted_term_str(s: &str) -> IResult<&str> { +fn quoted_term_str(s: &str) -> IResult<'_, &str> { let (opened, _) = char('"')(s)?; if let Ok((tail, inner)) = escaped::<_, ParseError, _, _>(is_not(r#""\"#), '\\', anychar).parse(opened) @@ -321,7 +338,7 @@ fn quoted_term_str(s: &str) -> IResult<&str> { /// Determine if text is a qualified search, and handle escaped chars. /// Expect well-formed input: unempty and no trailing \. -fn search_node_for_text(s: &str) -> ParseResult { +fn search_node_for_text(s: &str) -> ParseResult<'_, SearchNode> { // leading : is only possible error for well-formed input let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| { !t.is_empty() @@ -369,21 +386,26 @@ fn search_node_for_text_with_argument<'a>( }) } -fn parse_tag(s: &str) -> ParseResult { +fn parse_tag(s: &str) -> ParseResult<'_, SearchNode> { Ok(if let Some(re) = s.strip_prefix("re:") { SearchNode::Tag { tag: unescape_quotes(re), - is_re: true, + mode: FieldSearchMode::Regex, + } + } else if let Some(nc) = s.strip_prefix("nc:") { + SearchNode::Tag { + tag: unescape(nc)?, + mode: FieldSearchMode::NoCombining, } } else { SearchNode::Tag { tag: unescape(s)?, - is_re: false, + mode: FieldSearchMode::Normal, } }) } -fn parse_template(s: &str) -> ParseResult { +fn parse_template(s: &str) -> ParseResult<'_, SearchNode> { Ok(SearchNode::CardTemplate(match s.parse::() { Ok(n) => TemplateKind::Ordinal(n.max(1) - 1), Err(_) => TemplateKind::Name(unescape(s)?), @@ -391,7 +413,7 @@ fn parse_template(s: &str) -> ParseResult { } /// flag:0-7 -fn parse_flag(s: &str) -> ParseResult { +fn parse_flag(s: &str) -> ParseResult<'_, SearchNode> { if let Ok(flag) = s.parse::() { if flag > 7 { Err(parse_failure(s, FailKind::InvalidFlag)) @@ -404,7 +426,7 @@ fn parse_flag(s: &str) -> ParseResult { } /// eg resched:3 -fn parse_resched(s: &str) -> ParseResult { +fn parse_resched(s: &str) -> ParseResult<'_, SearchNode> { parse_u32(s, "resched:").map(|days| SearchNode::Rated { days, ease: RatingKind::ManualReschedule, @@ -412,7 +434,7 @@ fn parse_resched(s: &str) -> ParseResult { } /// eg prop:ivl>3, prop:ease!=2.5 -fn parse_prop(prop_clause: &str) -> ParseResult { +fn parse_prop(prop_clause: &str) -> ParseResult<'_, SearchNode> { let (tail, prop) = alt(( tag("ivl"), tag("due"), @@ -580,23 +602,23 @@ fn parse_prop_rated<'a>(num: &str, context: &'a str) -> ParseResult<'a, Property } /// eg added:1 -fn parse_added(s: &str) -> ParseResult { +fn parse_added(s: &str) -> ParseResult<'_, SearchNode> { parse_u32(s, "added:").map(|n| SearchNode::AddedInDays(n.max(1))) } /// eg edited:1 -fn parse_edited(s: &str) -> ParseResult { +fn parse_edited(s: &str) -> ParseResult<'_, SearchNode> { parse_u32(s, "edited:").map(|n| SearchNode::EditedInDays(n.max(1))) } /// eg introduced:1 -fn parse_introduced(s: &str) -> ParseResult { +fn parse_introduced(s: &str) -> ParseResult<'_, SearchNode> { parse_u32(s, "introduced:").map(|n| SearchNode::IntroducedInDays(n.max(1))) } /// eg rated:3 or rated:10:2 /// second arg must be between 1-4 -fn parse_rated(s: &str) -> ParseResult { +fn parse_rated(s: &str) -> ParseResult<'_, SearchNode> { let mut it = s.splitn(2, ':'); let days = parse_u32(it.next().unwrap(), "rated:")?.max(1); let button = parse_answer_button(it.next(), s)?; @@ -604,7 +626,7 @@ fn parse_rated(s: &str) -> ParseResult { } /// eg is:due -fn parse_state(s: &str) -> ParseResult { +fn parse_state(s: &str) -> ParseResult<'_, SearchNode> { use StateKind::*; Ok(SearchNode::State(match s { "new" => New, @@ -624,7 +646,7 @@ fn parse_state(s: &str) -> ParseResult { })) } -fn parse_mid(s: &str) -> ParseResult { +fn parse_mid(s: &str) -> ParseResult<'_, SearchNode> { parse_i64(s, "mid:").map(|n| SearchNode::NotetypeId(n.into())) } @@ -646,7 +668,7 @@ fn check_id_list<'a>(s: &'a str, context: &str) -> ParseResult<'a, &'a str> { } /// eg dupe:1231,hello -fn parse_dupe(s: &str) -> ParseResult { +fn parse_dupe(s: &str) -> ParseResult<'_, SearchNode> { let mut it = s.splitn(2, ','); let ntid = parse_i64(it.next().unwrap(), s)?; if let Some(text) = it.next() { @@ -670,13 +692,19 @@ fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult<'a, SearchN SearchNode::SingleField { field: unescape(key)?, text: unescape_quotes(stripped), - is_re: true, + mode: FieldSearchMode::Regex, + } + } else if let Some(stripped) = val.strip_prefix("nc:") { + SearchNode::SingleField { + field: unescape(key)?, + text: unescape_quotes(stripped), + mode: FieldSearchMode::NoCombining, } } else { SearchNode::SingleField { field: unescape(key)?, text: unescape(val)?, - is_re: false, + mode: FieldSearchMode::Normal, } }) } @@ -700,7 +728,7 @@ fn unescape_quotes_and_backslashes(s: &str) -> String { } /// Unescape chars with special meaning to the parser. -fn unescape(txt: &str) -> ParseResult { +fn unescape(txt: &str) -> ParseResult<'_, String> { if let Some(seq) = invalid_escape_sequence(txt) { Err(parse_failure( txt, @@ -806,7 +834,7 @@ mod test { Search(SingleField { field: "foo".into(), text: "bar baz".into(), - is_re: false, + mode: FieldSearchMode::Normal, }) ]))), Or, @@ -819,7 +847,16 @@ mod test { vec![Search(SingleField { field: "foo".into(), text: "bar".into(), - is_re: true + mode: FieldSearchMode::Regex, + })] + ); + + assert_eq!( + parse("foo:nc:bar")?, + vec![Search(SingleField { + field: "foo".into(), + text: "bar".into(), + mode: FieldSearchMode::NoCombining, })] ); @@ -829,7 +866,7 @@ mod test { vec![Search(SingleField { field: "field".into(), text: "va\"lue".into(), - is_re: false + mode: FieldSearchMode::Normal, })] ); assert_eq!(parse(r#""field:va\"lue""#)?, parse(r#"field:"va\"lue""#)?,); @@ -906,14 +943,14 @@ mod test { parse("tag:hard")?, vec![Search(Tag { tag: "hard".into(), - is_re: false + mode: FieldSearchMode::Normal })] ); assert_eq!( parse(r"tag:re:\\")?, vec![Search(Tag { tag: r"\\".into(), - is_re: true + mode: FieldSearchMode::Regex })] ); assert_eq!( diff --git a/rslib/src/search/service/search_node.rs b/rslib/src/search/service/search_node.rs index 1851a28f7..6986eef2a 100644 --- a/rslib/src/search/service/search_node.rs +++ b/rslib/src/search/service/search_node.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use crate::prelude::*; use crate::search::parse_search; +use crate::search::FieldSearchMode; use crate::search::Negated; use crate::search::Node; use crate::search::PropertyKind; @@ -40,7 +41,7 @@ impl TryFrom for Node { Filter::FieldName(s) => Node::Search(SearchNode::SingleField { field: escape_anki_wildcards_for_search_node(&s), text: "_*".to_string(), - is_re: false, + mode: FieldSearchMode::Normal, }), Filter::Rated(rated) => Node::Search(SearchNode::Rated { days: rated.days, @@ -107,7 +108,7 @@ impl TryFrom for Node { Filter::Field(field) => Node::Search(SearchNode::SingleField { field: escape_anki_wildcards(&field.field_name), text: escape_anki_wildcards(&field.text), - is_re: field.is_re, + mode: field.mode().into(), }), Filter::LiteralText(text) => { let text = escape_anki_wildcards(&text); diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 542dba4fc..f6237d6fd 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -7,6 +7,7 @@ use std::ops::Range; use itertools::Itertools; +use super::parser::FieldSearchMode; use super::parser::Node; use super::parser::PropertyKind; use super::parser::RatingKind; @@ -138,8 +139,8 @@ impl SqlWriter<'_> { false, )? } - SearchNode::SingleField { field, text, is_re } => { - self.write_field(&norm(field), &self.norm_note(text), *is_re)? + SearchNode::SingleField { field, text, mode } => { + self.write_field(&norm(field), &self.norm_note(text), *mode)? } SearchNode::Duplicates { notetype_id, text } => { self.write_dupe(*notetype_id, &self.norm_note(text))? @@ -180,7 +181,7 @@ impl SqlWriter<'_> { SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)), SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?, - SearchNode::Tag { tag, is_re } => self.write_tag(&norm(tag), *is_re), + SearchNode::Tag { tag, mode } => self.write_tag(&norm(tag), *mode), SearchNode::State(state) => self.write_state(state)?, SearchNode::Flag(flag) => { write!(self.sql, "(c.flags & 7) == {flag}").unwrap(); @@ -296,8 +297,8 @@ impl SqlWriter<'_> { Ok(()) } - fn write_tag(&mut self, tag: &str, is_re: bool) { - if is_re { + fn write_tag(&mut self, tag: &str, mode: FieldSearchMode) { + if mode == FieldSearchMode::Regex { self.args.push(format!("(?i){tag}")); write!(self.sql, "regexp_tags(?{}, n.tags)", self.args.len()).unwrap(); } else { @@ -310,8 +311,19 @@ impl SqlWriter<'_> { } s if s.contains(' ') => write!(self.sql, "false").unwrap(), text => { - write!(self.sql, "n.tags regexp ?").unwrap(); - let re = &to_custom_re(text, r"\S"); + let text = if mode == FieldSearchMode::Normal { + write!(self.sql, "n.tags regexp ?").unwrap(); + Cow::from(text) + } else { + write!( + self.sql, + "coalesce(process_text(n.tags, {}), n.tags) regexp ?", + ProcessTextFlags::NoCombining.bits() + ) + .unwrap(); + without_combining(text) + }; + let re = &to_custom_re(&text, r"\S"); self.args.push(format!("(?i).* {re}(::| ).*")); } } @@ -567,16 +579,18 @@ impl SqlWriter<'_> { } } - fn write_field(&mut self, field_name: &str, val: &str, is_re: bool) -> Result<()> { + fn write_field(&mut self, field_name: &str, val: &str, mode: FieldSearchMode) -> Result<()> { if matches!(field_name, "*" | "_*" | "*_") { - if is_re { + if mode == FieldSearchMode::Regex { self.write_all_fields_regexp(val); } else { self.write_all_fields(val); } Ok(()) - } else if is_re { + } else if mode == FieldSearchMode::Regex { self.write_single_field_regexp(field_name, val) + } else if mode == FieldSearchMode::NoCombining { + self.write_single_field_nc(field_name, val) } else { self.write_single_field(field_name, val) } @@ -592,6 +606,58 @@ impl SqlWriter<'_> { write!(self.sql, "regexp_fields(?{}, n.flds)", self.args.len()).unwrap(); } + fn write_single_field_nc(&mut self, field_name: &str, val: &str) -> Result<()> { + let field_indicies_by_notetype = self.num_fields_and_fields_indices_by_notetype( + field_name, + matches!(val, "*" | "_*" | "*_"), + )?; + if field_indicies_by_notetype.is_empty() { + write!(self.sql, "false").unwrap(); + return Ok(()); + } + + let val = to_sql(val); + let val = without_combining(&val); + self.args.push(val.into()); + let arg_idx = self.args.len(); + let field_idx_str = format!("' || ?{arg_idx} || '"); + let other_idx_str = "%".to_string(); + + let notetype_clause = |ctx: &FieldQualifiedSearchContext| -> String { + let field_index_clause = |range: &Range| { + let f = (0..ctx.total_fields_in_note) + .filter_map(|i| { + if i as u32 == range.start { + Some(&field_idx_str) + } else if range.contains(&(i as u32)) { + None + } else { + Some(&other_idx_str) + } + }) + .join("\x1f"); + format!( + "coalesce(process_text(n.flds, {}), n.flds) like '{f}' escape '\\'", + ProcessTextFlags::NoCombining.bits() + ) + }; + + let all_field_clauses = ctx + .field_ranges_to_search + .iter() + .map(field_index_clause) + .join(" or "); + format!("(n.mid = {mid} and ({all_field_clauses}))", mid = ctx.ntid) + }; + let all_notetype_clauses = field_indicies_by_notetype + .iter() + .map(notetype_clause) + .join(" or "); + write!(self.sql, "({all_notetype_clauses})").unwrap(); + + Ok(()) + } + fn write_single_field_regexp(&mut self, field_name: &str, val: &str) -> Result<()> { let field_indicies_by_notetype = self.fields_indices_by_notetype(field_name)?; if field_indicies_by_notetype.is_empty() { @@ -1116,6 +1182,20 @@ mod test { vec!["(?i)te.*st".into()] ) ); + // field search with no-combine + assert_eq!( + s(ctx, "front:nc:frânçais"), + ( + concat!( + "(((n.mid = 1581236385344 and (coalesce(process_text(n.flds, 1), n.flds) like '' || ?1 || '\u{1f}%' escape '\\')) or ", + "(n.mid = 1581236385345 and (coalesce(process_text(n.flds, 1), n.flds) like '' || ?1 || '\u{1f}%\u{1f}%' escape '\\')) or ", + "(n.mid = 1581236385346 and (coalesce(process_text(n.flds, 1), n.flds) like '' || ?1 || '\u{1f}%' escape '\\')) or ", + "(n.mid = 1581236385347 and (coalesce(process_text(n.flds, 1), n.flds) like '' || ?1 || '\u{1f}%' escape '\\'))))" + ) + .into(), + vec!["francais".into()] + ) + ); // all field search assert_eq!( s(ctx, "*:te*st"), diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 3bbe6fd0a..68d05c66d 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -9,6 +9,7 @@ use regex::Regex; use crate::notetype::NotetypeId as NotetypeIdType; use crate::prelude::*; use crate::search::parser::parse; +use crate::search::parser::FieldSearchMode; use crate::search::parser::Node; use crate::search::parser::PropertyKind; use crate::search::parser::RatingKind; @@ -69,7 +70,7 @@ fn write_search_node(node: &SearchNode) -> String { use SearchNode::*; match node { UnqualifiedText(s) => maybe_quote(&s.replace(':', "\\:")), - SingleField { field, text, is_re } => write_single_field(field, text, *is_re), + SingleField { field, text, mode } => write_single_field(field, text, *mode), AddedInDays(u) => format!("added:{u}"), EditedInDays(u) => format!("edited:{u}"), IntroducedInDays(u) => format!("introduced:{u}"), @@ -81,7 +82,7 @@ fn write_search_node(node: &SearchNode) -> String { NotetypeId(NotetypeIdType(i)) => format!("mid:{i}"), Notetype(s) => maybe_quote(&format!("note:{s}")), Rated { days, ease } => write_rated(days, ease), - Tag { tag, is_re } => write_single_field("tag", tag, *is_re), + Tag { tag, mode } => write_single_field("tag", tag, *mode), Duplicates { notetype_id, text } => write_dupe(notetype_id, text), State(k) => write_state(k), Flag(u) => format!("flag:{u}"), @@ -116,14 +117,25 @@ fn needs_quotation(txt: &str) -> bool { } /// Also used by tag search, which has the same syntax. -fn write_single_field(field: &str, text: &str, is_re: bool) -> String { - let re = if is_re { "re:" } else { "" }; - let text = if !is_re && text.starts_with("re:") { +fn write_single_field(field: &str, text: &str, mode: FieldSearchMode) -> String { + let prefix = match mode { + FieldSearchMode::Normal => "", + FieldSearchMode::Regex => "re:", + FieldSearchMode::NoCombining => "nc:", + }; + let text = if mode == FieldSearchMode::Normal + && (text.starts_with("re:") || text.starts_with("nc:")) + { text.replacen(':', "\\:", 1) } else { text.to_string() }; - maybe_quote(&format!("{}:{}{}", field.replace(':', "\\:"), re, &text)) + maybe_quote(&format!( + "{}:{}{}", + field.replace(':', "\\:"), + prefix, + &text + )) } fn write_template(template: &TemplateKind) -> String { diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 008977fe9..0dabff5e5 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -76,8 +76,15 @@ impl Collection { note_id: card.note_id.into(), deck: deck.human_name(), added: card.id.as_secs().0, - first_review: revlog.first().map(|entry| entry.id.as_secs().0), - latest_review: revlog.last().map(|entry| entry.id.as_secs().0), + first_review: revlog + .iter() + .find(|entry| entry.has_rating()) + .map(|entry| entry.id.as_secs().0), + // last_review_time is not used to ensure cram revlogs are included. + latest_review: revlog + .iter() + .rfind(|entry| entry.has_rating()) + .map(|entry| entry.id.as_secs().0), due_date: self.due_date(&card)?, due_position: self.position(&card), interval: card.interval, @@ -220,6 +227,7 @@ fn stats_revlog_entry( ease: entry.ease_factor, taken_secs: entry.taken_millis as f32 / 1000., memory_state: None, + last_interval: entry.last_interval_secs(), } } diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 1d0d62fd7..3a5066ff4 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -403,7 +403,9 @@ impl super::SqliteStorage { let last_revlog_info = get_last_revlog_info(&revlog); for (card_id, last_revlog_info) in last_revlog_info { let card = self.get_card(card_id)?; - if let Some(mut card) = card { + if last_revlog_info.last_reviewed_at.is_none() { + continue; + } else if let Some(mut card) = card { if card.ctype != CardType::New && card.last_review_time.is_none() { card.last_review_time = last_revlog_info.last_reviewed_at; self.update_card(&card)?; diff --git a/rslib/src/tags/register.rs b/rslib/src/tags/register.rs index 9605ea92a..d02fa260a 100644 --- a/rslib/src/tags/register.rs +++ b/rslib/src/tags/register.rs @@ -155,7 +155,7 @@ fn invalid_char_for_tag(c: char) -> bool { c.is_ascii_control() || is_tag_separator(c) } -fn normalized_tag_name_component(comp: &str) -> Cow { +fn normalized_tag_name_component(comp: &str) -> Cow<'_, str> { let mut out = normalize_to_nfc(comp); if out.contains(invalid_char_for_tag) { out = out.replace(invalid_char_for_tag, "").into(); @@ -170,7 +170,7 @@ fn normalized_tag_name_component(comp: &str) -> Cow { } } -pub(super) fn normalize_tag_name(name: &str) -> Result> { +pub(super) fn normalize_tag_name(name: &str) -> Result> { let normalized_name: Cow = if name .split("::") .any(|comp| matches!(normalized_tag_name_component(comp), Cow::Owned(_))) diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 4895cc162..aa84e0e7f 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -121,7 +121,7 @@ pub enum Token<'a> { CloseConditional(&'a str), } -fn comment_token(s: &str) -> nom::IResult<&str, Token> { +fn comment_token(s: &str) -> nom::IResult<&str, Token<'_>> { map( delimited( tag(COMMENT_START), @@ -151,7 +151,7 @@ fn tokens(mut template: &str) -> impl Iterator>> } /// classify handle based on leading character -fn classify_handle(s: &str) -> Token { +fn classify_handle(s: &str) -> Token<'_> { let start = s.trim_start_matches('{').trim(); if start.len() < 2 { return Token::Replacement(start); diff --git a/rslib/src/template_filters.rs b/rslib/src/template_filters.rs index 4949e756d..66e9ecb37 100644 --- a/rslib/src/template_filters.rs +++ b/rslib/src/template_filters.rs @@ -117,7 +117,7 @@ fn captured_sound(caps: &Captures) -> bool { caps.get(2).unwrap().as_str().starts_with("sound:") } -fn kana_filter(text: &str) -> Cow { +fn kana_filter(text: &str) -> Cow<'_, str> { FURIGANA .replace_all(&text.replace(" ", " "), |caps: &Captures| { if captured_sound(caps) { @@ -130,7 +130,7 @@ fn kana_filter(text: &str) -> Cow { .into() } -fn kanji_filter(text: &str) -> Cow { +fn kanji_filter(text: &str) -> Cow<'_, str> { FURIGANA .replace_all(&text.replace(" ", " "), |caps: &Captures| { if captured_sound(caps) { @@ -143,7 +143,7 @@ fn kanji_filter(text: &str) -> Cow { .into() } -fn furigana_filter(text: &str) -> Cow { +fn furigana_filter(text: &str) -> Cow<'_, str> { FURIGANA .replace_all(&text.replace(" ", " "), |caps: &Captures| { if captured_sound(caps) { diff --git a/rslib/src/text.rs b/rslib/src/text.rs index 590c05b39..037366c28 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -215,8 +215,8 @@ pub fn is_html(text: impl AsRef) -> bool { HTML.is_match(text.as_ref()) } -pub fn html_to_text_line(html: &str, preserve_media_filenames: bool) -> Cow { - let (html_stripper, sound_rep): (fn(&str) -> Cow, _) = if preserve_media_filenames { +pub fn html_to_text_line(html: &str, preserve_media_filenames: bool) -> Cow<'_, str> { + let (html_stripper, sound_rep): (fn(&str) -> Cow<'_, str>, _) = if preserve_media_filenames { (strip_html_preserving_media_filenames, "$1") } else { (strip_html, "") @@ -229,15 +229,15 @@ pub fn html_to_text_line(html: &str, preserve_media_filenames: bool) -> Cow .trim() } -pub fn strip_html(html: &str) -> Cow { +pub fn strip_html(html: &str) -> Cow<'_, str> { strip_html_preserving_entities(html).map_cow(decode_entities) } -pub fn strip_html_preserving_entities(html: &str) -> Cow { +pub fn strip_html_preserving_entities(html: &str) -> Cow<'_, str> { HTML.replace_all(html, "") } -pub fn decode_entities(html: &str) -> Cow { +pub fn decode_entities(html: &str) -> Cow<'_, str> { if html.contains('&') { match htmlescape::decode_html(html) { Ok(text) => text.replace('\u{a0}', " ").into(), @@ -249,7 +249,7 @@ pub fn decode_entities(html: &str) -> Cow { } } -pub(crate) fn newlines_to_spaces(text: &str) -> Cow { +pub(crate) fn newlines_to_spaces(text: &str) -> Cow<'_, str> { if text.contains('\n') { text.replace('\n', " ").into() } else { @@ -257,7 +257,7 @@ pub(crate) fn newlines_to_spaces(text: &str) -> Cow { } } -pub fn strip_html_for_tts(html: &str) -> Cow { +pub fn strip_html_for_tts(html: &str) -> Cow<'_, str> { HTML_LINEBREAK_TAGS .replace_all(html, " ") .map_cow(strip_html) @@ -282,7 +282,7 @@ pub(crate) struct MediaRef<'a> { pub fname_decoded: Cow<'a, str>, } -pub(crate) fn extract_media_refs(text: &str) -> Vec { +pub(crate) fn extract_media_refs(text: &str) -> Vec> { let mut out = vec![]; for caps in HTML_MEDIA_TAGS.captures_iter(text) { @@ -359,11 +359,11 @@ pub(crate) fn extract_underscored_references(text: &str) -> Vec<&str> { /// Returns the first matching group as a str. This is intended for regexes /// where exactly one group matches, and will panic for matches without matching /// groups. -fn extract_match(caps: Captures) -> &str { +fn extract_match(caps: Captures<'_>) -> &str { caps.iter().skip(1).find_map(|g| g).unwrap().as_str() } -pub fn strip_html_preserving_media_filenames(html: &str) -> Cow { +pub fn strip_html_preserving_media_filenames(html: &str) -> Cow<'_, str> { HTML_MEDIA_TAGS .replace_all(html, r" ${1}${2}${3} ") .map_cow(strip_html) @@ -385,7 +385,7 @@ pub(crate) fn sanitize_html_no_images(html: &str) -> String { .to_string() } -pub(crate) fn normalize_to_nfc(s: &str) -> Cow { +pub(crate) fn normalize_to_nfc(s: &str) -> Cow<'_, str> { match is_nfc(s) { false => s.chars().nfc().collect::().into(), true => s.into(), @@ -429,7 +429,7 @@ static EXTRA_NO_COMBINING_REPLACEMENTS: phf::Map = phf::phf_map! { }; /// Convert provided string to NFKD form and strip combining characters. -pub(crate) fn without_combining(s: &str) -> Cow { +pub(crate) fn without_combining(s: &str) -> Cow<'_, str> { // if the string is already normalized if matches!(is_nfkd_quick(s.chars()), IsNormalized::Yes) { // and no combining characters found, return unchanged @@ -472,7 +472,7 @@ pub(crate) fn is_glob(txt: &str) -> bool { } /// Convert to a RegEx respecting Anki wildcards. -pub(crate) fn to_re(txt: &str) -> Cow { +pub(crate) fn to_re(txt: &str) -> Cow<'_, str> { to_custom_re(txt, ".") } @@ -492,7 +492,7 @@ pub(crate) fn to_custom_re<'a>(txt: &'a str, wildcard: &str) -> Cow<'a, str> { } /// Convert to SQL respecting Anki wildcards. -pub(crate) fn to_sql(txt: &str) -> Cow { +pub(crate) fn to_sql(txt: &str) -> Cow<'_, str> { // escape sequences and unescaped special characters which need conversion static RE: LazyLock = LazyLock::new(|| Regex::new(r"\\[\\*]|[*%]").unwrap()); RE.replace_all(txt, |caps: &Captures| { @@ -508,7 +508,7 @@ pub(crate) fn to_sql(txt: &str) -> Cow { } /// Unescape everything. -pub(crate) fn to_text(txt: &str) -> Cow { +pub(crate) fn to_text(txt: &str) -> Cow<'_, str> { static RE: LazyLock = LazyLock::new(|| Regex::new(r"\\(.)").unwrap()); RE.replace_all(txt, "$1") } @@ -561,14 +561,14 @@ const FRAGMENT_QUERY_UNION: &AsciiSet = &CONTROLS .add(b'#'); /// IRI-encode unescaped local paths in HTML fragment. -pub(crate) fn encode_iri_paths(unescaped_html: &str) -> Cow { +pub(crate) fn encode_iri_paths(unescaped_html: &str) -> Cow<'_, str> { transform_html_paths(unescaped_html, |fname| { utf8_percent_encode(fname, FRAGMENT_QUERY_UNION).into() }) } /// URI-decode escaped local paths in HTML fragment. -pub(crate) fn decode_iri_paths(escaped_html: &str) -> Cow { +pub(crate) fn decode_iri_paths(escaped_html: &str) -> Cow<'_, str> { transform_html_paths(escaped_html, |fname| { percent_decode_str(fname).decode_utf8_lossy() }) @@ -577,9 +577,9 @@ pub(crate) fn decode_iri_paths(escaped_html: &str) -> Cow { /// Apply a transform to local filename references in tags like IMG. /// Required at display time, as Anki unfortunately stores the references /// in unencoded form in the database. -fn transform_html_paths(html: &str, transform: F) -> Cow +fn transform_html_paths(html: &str, transform: F) -> Cow<'_, str> where - F: Fn(&str) -> Cow, + F: Fn(&str) -> Cow<'_, str>, { HTML_MEDIA_TAGS.replace_all(html, |caps: &Captures| { let fname = caps diff --git a/rslib/src/typeanswer.rs b/rslib/src/typeanswer.rs index 1432ad50d..9bf3dc47c 100644 --- a/rslib/src/typeanswer.rs +++ b/rslib/src/typeanswer.rs @@ -49,7 +49,7 @@ pub fn compare_answer(expected: &str, typed: &str, combining: bool) -> String { trait DiffTrait { fn get_typed(&self) -> &[char]; fn get_expected(&self) -> &[char]; - fn get_expected_original(&self) -> Cow; + fn get_expected_original(&self) -> Cow<'_, str>; fn new(expected: &str, typed: &str) -> Self; @@ -58,7 +58,7 @@ trait DiffTrait { if self.get_typed() == self.get_expected() { format_typeans!(format!( "{}", - self.get_expected_original() + htmlescape::encode_minimal(&self.get_expected_original()) )) } else { let output = self.to_tokens(); @@ -136,7 +136,7 @@ fn render_tokens(tokens: &[DiffToken]) -> String { /// Prefixes a leading mark character with a non-breaking space to prevent /// it from joining the previous token. -fn isolate_leading_mark(text: &str) -> Cow { +fn isolate_leading_mark(text: &str) -> Cow<'_, str> { if text .chars() .next() @@ -161,7 +161,7 @@ impl DiffTrait for Diff { fn get_expected(&self) -> &[char] { &self.expected } - fn get_expected_original(&self) -> Cow { + fn get_expected_original(&self) -> Cow<'_, str> { Cow::Owned(self.get_expected().iter().collect::()) } @@ -191,7 +191,7 @@ impl DiffTrait for DiffNonCombining { fn get_expected(&self) -> &[char] { &self.base.expected } - fn get_expected_original(&self) -> Cow { + fn get_expected_original(&self) -> Cow<'_, str> { Cow::Borrowed(&self.expected_original) } @@ -391,6 +391,15 @@ mod test { assert_eq!(ctx, "123"); } + #[test] + fn correct_input_is_escaped() { + let ctx = Diff::new("source /bin/activate", "source /bin/activate"); + assert_eq!( + ctx.to_html(), + "source <dir>/bin/activate" + ); + } + #[test] fn correct_input_is_collapsed() { let ctx = Diff::new("123", "123"); diff --git a/run.bat b/run.bat index c689dda16..aecbf2491 100755 --- a/run.bat +++ b/run.bat @@ -9,6 +9,8 @@ set QTWEBENGINE_CHROMIUM_FLAGS=--remote-allow-origins=http://localhost:8080 set ANKI_API_PORT=40000 set ANKI_API_HOST=127.0.0.1 +@if not defined PYENV set PYENV=out\pyenv + call tools\ninja pylib qt || exit /b 1 -.\out\pyenv\scripts\python tools\run.py %* || exit /b 1 +%PYENV%\Scripts\python tools\run.py %* || exit /b 1 popd diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 8a21ec74e..452c65213 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] # older versions may fail to compile; newer versions may fail the clippy tests -channel = "1.88.0" +channel = "1.89.0" diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs index c99fbe06e..6d38278b5 100644 --- a/tools/minilints/src/main.rs +++ b/tools/minilints/src/main.rs @@ -255,9 +255,7 @@ fn check_for_unstaged_changes() { } fn generate_licences() -> Result { - if which::which("cargo-license").is_err() { - Command::run("cargo install cargo-license@0.5.1")?; - } + Command::run("cargo install cargo-license@0.7.0")?; let output = Command::run_with_output([ "cargo-license", "--features", diff --git a/tools/run.py b/tools/run.py index da0baa2c4..e17e22a97 100644 --- a/tools/run.py +++ b/tools/run.py @@ -5,8 +5,6 @@ import os import sys sys.path.extend(["pylib", "qt", "out/pylib", "out/qt"]) -if sys.platform == "win32": - os.environ["PATH"] += ";out\\extracted\\win_amd64_audio" import aqt diff --git a/ts/editable/Mathjax.svelte b/ts/editable/Mathjax.svelte index 74fbbba43..65cf570ee 100644 --- a/ts/editable/Mathjax.svelte +++ b/ts/editable/Mathjax.svelte @@ -31,7 +31,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - + saveCustomColours({})} +/> {#if keyCombination} inputRef.click()} /> diff --git a/ts/editor/editor-toolbar/HighlightColorButton.svelte b/ts/editor/editor-toolbar/HighlightColorButton.svelte index 865ec5668..f89f7a99a 100644 --- a/ts/editor/editor-toolbar/HighlightColorButton.svelte +++ b/ts/editor/editor-toolbar/HighlightColorButton.svelte @@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import ColorPicker from "./ColorPicker.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import WithColorHelper from "./WithColorHelper.svelte"; + import { saveCustomColours } from "@generated/backend"; export let color: string; @@ -134,7 +135,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html color = setColor(event); bridgeCommand(`lastHighlightColor:${color}`); }} - on:change={() => setTextColor()} + on:change={() => { + setTextColor(); + saveCustomColours({}); + }} /> diff --git a/ts/editor/editor-toolbar/TextColorButton.svelte b/ts/editor/editor-toolbar/TextColorButton.svelte index 165953180..ce80aae49 100644 --- a/ts/editor/editor-toolbar/TextColorButton.svelte +++ b/ts/editor/editor-toolbar/TextColorButton.svelte @@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import ColorPicker from "./ColorPicker.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import WithColorHelper from "./WithColorHelper.svelte"; + import { saveCustomColours } from "@generated/backend"; export let color: string; @@ -158,6 +159,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setTimeout(() => { setTextColor(); }, 200); + saveCustomColours({}); }} /> diff --git a/ts/lib/components/HelpModal.svelte b/ts/lib/components/HelpModal.svelte index cf6292537..7ee425950 100644 --- a/ts/lib/components/HelpModal.svelte +++ b/ts/lib/components/HelpModal.svelte @@ -23,6 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let title: string; export let url: string; + export let linkLabel: string | undefined = undefined; export let startIndex = 0; export let helpSections: HelpItem[]; export let fsrs = false; @@ -106,11 +107,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{@html renderMarkdown( tr.helpForMoreInfo({ - link: `${title}`, + link: `${linkLabel ?? title}`, }), )}
diff --git a/ts/lib/tslib/help-page.ts b/ts/lib/tslib/help-page.ts index e3f209c6a..e2b2e3da4 100644 --- a/ts/lib/tslib/help-page.ts +++ b/ts/lib/tslib/help-page.ts @@ -27,7 +27,8 @@ export const HelpPage = { limitsFromTop: "https://docs.ankiweb.net/deck-options.html#limits-start-from-top", dailyLimits: "https://docs.ankiweb.net/deck-options.html#daily-limits", audio: "https://docs.ankiweb.net/deck-options.html#audio", - fsrs: "http://docs.ankiweb.net/deck-options.html#fsrs", + fsrs: "https://docs.ankiweb.net/deck-options.html#fsrs", + desiredRetention: "https://docs.ankiweb.net/deck-options.html#desired-retention", }, Leeches: { leeches: "https://docs.ankiweb.net/leeches.html#leeches", diff --git a/ts/lib/tslib/uuid.ts b/ts/lib/tslib/uuid.ts deleted file mode 100644 index 8598261b0..000000000 --- a/ts/lib/tslib/uuid.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -/** - * TODO replace with crypto.randomUUID - */ -export function randomUUID(): string { - const value = `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`; - - return value.replace(/[018]/g, (character: string): string => - ( - Number(character) - ^ (crypto.getRandomValues(new Uint8Array(1))[0] - & (15 >> (Number(character) / 4))) - ).toString(16)); -} diff --git a/ts/licenses.json b/ts/licenses.json index 412d1dae3..95eef0cec 100644 --- a/ts/licenses.json +++ b/ts/licenses.json @@ -95,8 +95,8 @@ "repository": "https://github.com/TooTallNate/node-agent-base", "publisher": "Nathan Rajlich", "email": "nathan@tootallnate.net", - "path": "node_modules/https-proxy-agent/node_modules/agent-base", - "licenseFile": "node_modules/https-proxy-agent/node_modules/agent-base/README.md" + "path": "node_modules/http-proxy-agent/node_modules/agent-base", + "licenseFile": "node_modules/http-proxy-agent/node_modules/agent-base/README.md" }, "asynckit@0.4.0": { "licenses": "MIT", @@ -127,6 +127,14 @@ "path": "node_modules/browser-process-hrtime", "licenseFile": "node_modules/browser-process-hrtime/LICENSE" }, + "call-bind-apply-helpers@1.0.2": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/call-bind-apply-helpers", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/call-bind-apply-helpers", + "licenseFile": "node_modules/call-bind-apply-helpers/LICENSE" + }, "codemirror@5.65.18": { "licenses": "MIT", "repository": "https://github.com/codemirror/CodeMirror", @@ -436,10 +444,58 @@ "path": "node_modules/domexception", "licenseFile": "node_modules/domexception/LICENSE.txt" }, + "dunder-proto@1.0.1": { + "licenses": "MIT", + "repository": "https://github.com/es-shims/dunder-proto", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/dunder-proto", + "licenseFile": "node_modules/dunder-proto/LICENSE" + }, "empty-npm-package@1.0.0": { "licenses": "ISC", "path": "node_modules/canvas" }, + "es-define-property@1.0.1": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/es-define-property", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-set-tostringtag/node_modules/es-define-property", + "licenseFile": "node_modules/es-set-tostringtag/node_modules/es-define-property/LICENSE" + }, + "es-errors@1.3.0": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/es-errors", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-errors", + "licenseFile": "node_modules/es-errors/LICENSE" + }, + "es-object-atoms@1.0.0": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/es-object-atoms", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-object-atoms", + "licenseFile": "node_modules/es-object-atoms/LICENSE" + }, + "es-object-atoms@1.1.1": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/es-object-atoms", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-set-tostringtag/node_modules/es-object-atoms", + "licenseFile": "node_modules/es-set-tostringtag/node_modules/es-object-atoms/LICENSE" + }, + "es-set-tostringtag@2.1.0": { + "licenses": "MIT", + "repository": "https://github.com/es-shims/es-set-tostringtag", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-set-tostringtag", + "licenseFile": "node_modules/es-set-tostringtag/LICENSE" + }, "escodegen@2.1.0": { "licenses": "BSD-2-Clause", "repository": "https://github.com/estools/escodegen", @@ -474,7 +530,7 @@ "path": "node_modules/fabric", "licenseFile": "node_modules/fabric/LICENSE" }, - "form-data@4.0.1": { + "form-data@4.0.4": { "licenses": "MIT", "repository": "https://github.com/form-data/form-data", "publisher": "Felix Geisendörfer", @@ -482,6 +538,38 @@ "path": "node_modules/form-data", "licenseFile": "node_modules/form-data/License" }, + "function-bind@1.1.2": { + "licenses": "MIT", + "repository": "https://github.com/Raynos/function-bind", + "publisher": "Raynos", + "email": "raynos2@gmail.com", + "path": "node_modules/function-bind", + "licenseFile": "node_modules/function-bind/LICENSE" + }, + "get-intrinsic@1.3.0": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/get-intrinsic", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-set-tostringtag/node_modules/get-intrinsic", + "licenseFile": "node_modules/es-set-tostringtag/node_modules/get-intrinsic/LICENSE" + }, + "get-proto@1.0.1": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/get-proto", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/get-proto", + "licenseFile": "node_modules/get-proto/LICENSE" + }, + "gopd@1.2.0": { + "licenses": "MIT", + "repository": "https://github.com/ljharb/gopd", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/dunder-proto/node_modules/gopd", + "licenseFile": "node_modules/dunder-proto/node_modules/gopd/LICENSE" + }, "hammerjs@2.0.8": { "licenses": "MIT", "repository": "https://github.com/hammerjs/hammer.js", @@ -490,6 +578,38 @@ "path": "node_modules/hammerjs", "licenseFile": "node_modules/hammerjs/LICENSE.md" }, + "has-symbols@1.0.3": { + "licenses": "MIT", + "repository": "https://github.com/inspect-js/has-symbols", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/has-symbols", + "licenseFile": "node_modules/has-symbols/LICENSE" + }, + "has-symbols@1.1.0": { + "licenses": "MIT", + "repository": "https://github.com/inspect-js/has-symbols", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/es-set-tostringtag/node_modules/has-symbols", + "licenseFile": "node_modules/es-set-tostringtag/node_modules/has-symbols/LICENSE" + }, + "has-tostringtag@1.0.2": { + "licenses": "MIT", + "repository": "https://github.com/inspect-js/has-tostringtag", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/has-tostringtag", + "licenseFile": "node_modules/has-tostringtag/LICENSE" + }, + "hasown@2.0.2": { + "licenses": "MIT", + "repository": "https://github.com/inspect-js/hasOwn", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/hasown", + "licenseFile": "node_modules/hasown/LICENSE" + }, "html-encoding-sniffer@3.0.0": { "licenses": "MIT", "repository": "https://github.com/jsdom/html-encoding-sniffer", @@ -587,6 +707,14 @@ "path": "node_modules/marked", "licenseFile": "node_modules/marked/LICENSE.md" }, + "math-intrinsics@1.1.0": { + "licenses": "MIT", + "repository": "https://github.com/es-shims/math-intrinsics", + "publisher": "Jordan Harband", + "email": "ljharb@gmail.com", + "path": "node_modules/math-intrinsics", + "licenseFile": "node_modules/math-intrinsics/LICENSE" + }, "mathjax@3.2.2": { "licenses": "Apache-2.0", "repository": "https://github.com/mathjax/MathJax", diff --git a/ts/reviewer/images.ts b/ts/reviewer/images.ts index 05de24158..28c54bebb 100644 --- a/ts/reviewer/images.ts +++ b/ts/reviewer/images.ts @@ -10,6 +10,9 @@ export function allImagesLoaded(): Promise { } function imageLoaded(img: HTMLImageElement): Promise { + if (!img.getAttribute("decoding")) { + img.decoding = "async"; + } return img.complete ? Promise.resolve() : new Promise((resolve) => { diff --git a/ts/routes/+error.svelte b/ts/routes/+error.svelte index 8f171d33d..3dfb64e22 100644 --- a/ts/routes/+error.svelte +++ b/ts/routes/+error.svelte @@ -3,9 +3,9 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> {message} diff --git a/ts/routes/card-info/[cardId]/+page.svelte b/ts/routes/card-info/[cardId]/+page.svelte index a1e137e10..af0b0997c 100644 --- a/ts/routes/card-info/[cardId]/+page.svelte +++ b/ts/routes/card-info/[cardId]/+page.svelte @@ -3,7 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> @@ -368,7 +377,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html class="btn btn-primary" on:click={() => { simulateFsrsRequest.reviewLimit = 9999; - workloadModal?.show(); + showSimulatorModal(workloadModal); }} > {tr.deckConfigFsrsDesiredRetentionHelpMeDecideExperimental()} @@ -455,7 +464,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
diff --git a/ts/routes/deck-options/FsrsOptionsOuter.svelte b/ts/routes/deck-options/FsrsOptionsOuter.svelte index fa543b5fc..49d4c681d 100644 --- a/ts/routes/deck-options/FsrsOptionsOuter.svelte +++ b/ts/routes/deck-options/FsrsOptionsOuter.svelte @@ -59,11 +59,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html sched: HelpItemScheduler.FSRS, global: true, }, - computeOptimalRetention: { - title: tr.deckConfigComputeOptimalRetention(), - help: tr.deckConfigComputeOptimalRetentionTooltip4(), - sched: HelpItemScheduler.FSRS, - }, healthCheck: { title: tr.deckConfigHealthCheck(), help: diff --git a/ts/routes/graphs/TrueRetention.svelte b/ts/routes/graphs/TrueRetention.svelte index 4a9738831..12d17079b 100644 --- a/ts/routes/graphs/TrueRetention.svelte +++ b/ts/routes/graphs/TrueRetention.svelte @@ -72,7 +72,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html > { modal = e.detail.modal; diff --git a/ts/routes/image-occlusion/Toolbar.svelte b/ts/routes/image-occlusion/Toolbar.svelte index 8775de936..b00e42087 100644 --- a/ts/routes/image-occlusion/Toolbar.svelte +++ b/ts/routes/image-occlusion/Toolbar.svelte @@ -32,6 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html saveNeededStore, opacityStateStore, } from "./store"; + import { get } from "svelte/store"; import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index"; import { makeMaskTransparent, SHAPE_MASK_COLOR } from "./tools/lib"; import { enableSelectable, stopDraw } from "./tools/lib"; @@ -55,6 +56,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onWheelDragX, } from "./tools/tool-zoom"; import { fillMask } from "./tools/tool-fill"; + import { getCustomColours, saveCustomColours } from "@generated/backend"; export let canvas; export let iconSize; @@ -76,6 +78,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let colourRef: HTMLInputElement | undefined; const colour = writable(SHAPE_MASK_COLOR); + const customColorPickerPalette = writable([]); + + async function loadCustomColours() { + customColorPickerPalette.set( + (await getCustomColours({})).colours.filter( + (hex) => !hex.startsWith("#ffffff"), + ), + ); + } + function onClick(event: MouseEvent) { const upperCanvas = document.querySelector(".upper-canvas"); if (event.target == upperCanvas) { @@ -222,7 +234,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } onMount(() => { - opacityStateStore.set(maskOpacity); + maskOpacity = get(opacityStateStore); removeHandlers = singleCallback( on(document, "click", onClick), on(window, "mousemove", onMousemove), @@ -233,6 +245,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html on(document, "touchstart", onTouchstart), on(document, "mousemove", onMousemoveDocument), ); + loadCustomColours(); }); onDestroy(() => { @@ -241,7 +254,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - + + {#each $customColorPickerPalette as colour} + + {/each} ($colour = e.currentTarget!.value)} + on:change={() => saveCustomColours({})} />
diff --git a/ts/routes/image-occlusion/mask-editor.ts b/ts/routes/image-occlusion/mask-editor.ts index 6d4d0d284..41adbe423 100644 --- a/ts/routes/image-occlusion/mask-editor.ts +++ b/ts/routes/image-occlusion/mask-editor.ts @@ -8,10 +8,22 @@ import { fabric } from "fabric"; import { get } from "svelte/store"; import { optimumCssSizeForCanvas } from "./canvas-scale"; -import { hideAllGuessOne, notesDataStore, saveNeededStore, tagsWritable, textEditingState } from "./store"; +import { + hideAllGuessOne, + notesDataStore, + opacityStateStore, + saveNeededStore, + tagsWritable, + textEditingState, +} from "./store"; import Toast from "./Toast.svelte"; import { addShapesToCanvasFromCloze } from "./tools/add-from-cloze"; -import { enableSelectable, makeShapesRemainInCanvas, moveShapeToCanvasBoundaries } from "./tools/lib"; +import { + enableSelectable, + makeMaskTransparent, + makeShapesRemainInCanvas, + moveShapeToCanvasBoundaries, +} from "./tools/lib"; import { modifiedPolygon } from "./tools/tool-polygon"; import { undoStack } from "./tools/tool-undo-redo"; import { enablePinchZoom, onResize, setCanvasSize } from "./tools/tool-zoom"; @@ -83,6 +95,7 @@ export const setupMaskEditorForEdit = async ( window.requestAnimationFrame(() => { onImageLoaded({ noteId: BigInt(noteId) }); }); + if (get(opacityStateStore)) { makeMaskTransparent(canvas, true); } }; return canvas; diff --git a/yarn.lock b/yarn.lock index 54ba593bd..bc1640152 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3007,10 +3007,10 @@ __metadata: languageName: node linkType: hard -"devalue@npm:^5.1.0": - version: 5.1.1 - resolution: "devalue@npm:5.1.1" - checksum: 10c0/f6717a856fd54216959abd341cb189e47a9b37d72d8419e055ae77567ff4ed0fb683b1ffb6a71067f645adae5991bffabe6468a3e2385937bff49273e71c1f51 +"devalue@npm:^5.3.2": + version: 5.3.2 + resolution: "devalue@npm:5.3.2" + checksum: 10c0/2dab403779233224285afe4b30eaded038df10cb89b8f2c1e41dd855a8e6b634aa24175b87f64df665204bb9a6a6e7758d172682719b9c5cf3cef336ff9fa507 languageName: node linkType: hard @@ -6939,8 +6939,8 @@ __metadata: linkType: hard "vite@npm:6": - version: 6.3.5 - resolution: "vite@npm:6.3.5" + version: 6.4.1 + resolution: "vite@npm:6.4.1" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.4.4" @@ -6989,7 +6989,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c + checksum: 10c0/77bb4c5b10f2a185e7859cc9a81c789021bc18009b02900347d1583b453b58e4b19ff07a5e5a5b522b68fc88728460bb45a63b104d969e8c6a6152aea3b849f7 languageName: node linkType: hard