diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit index 05b9080e..e0686b6a 100644 --- a/.vite-hooks/pre-commit +++ b/.vite-hooks/pre-commit @@ -1 +1,2 @@ vp lint +vp staged diff --git a/Cargo.lock b/Cargo.lock index 477efc5a..41a3a94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,7 @@ dependencies = [ "clipboard-win", "image", "log 0.4.29", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", @@ -238,24 +238,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.1", - "raw-window-handle", - "serde", - "serde_repr", - "tokio", - "url", - "zbus", -] - [[package]] name = "assert_cmd" version = "2.1.2" @@ -686,7 +668,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", ] [[package]] @@ -1169,7 +1151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static 1.5.0", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1269,12 +1251,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.10.0" @@ -1346,6 +1322,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.1.3" @@ -1506,19 +1495,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.29.6" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", + "phf 0.13.1", "smallvec", - "syn 1.0.109", ] [[package]] @@ -1533,14 +1518,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.101", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" version = "0.20.11" @@ -1644,19 +1635,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.101", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -1672,7 +1650,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1727,24 +1705,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" -dependencies = [ - "bitflags 2.11.0", - "block2 0.6.1", - "libc", - "objc2 0.6.1", -] - [[package]] name = "dispatch2" version = "0.3.0" @@ -1752,7 +1712,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.1", + "block2 0.6.1", + "libc", + "objc2 0.6.4", ] [[package]] @@ -1798,6 +1760,21 @@ dependencies = [ "const-random", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1834,6 +1811,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -2252,16 +2244,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.31" @@ -2511,17 +2493,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -2529,10 +2500,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -2724,7 +2693,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2793,6 +2762,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.9.1" @@ -2837,14 +2812,12 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.29.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log 0.4.29", - "mac", "markup5ever", - "match_token", ] [[package]] @@ -2934,7 +2907,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -3018,12 +2990,12 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -3148,7 +3120,7 @@ dependencies = [ "bytemuck", "byteorder-lite", "num-traits", - "png", + "png 0.17.16", "tiff", ] @@ -3184,13 +3156,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.17.1", "serde", + "serde_core", ] [[package]] @@ -3283,16 +3256,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -3420,10 +3383,12 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -3511,18 +3476,6 @@ dependencies = [ "libc", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.9.0", - "selectors", -] - [[package]] name = "lazy_static" version = "0.2.11" @@ -3721,18 +3674,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "malloc_buf" version = "0.0.6" @@ -3744,35 +3685,15 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.14.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log 0.4.29", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" version = "0.7.3" @@ -3879,23 +3800,23 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.17", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4010,12 +3931,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5eb86a92577833b75522336f210c49d9ebd7dd55a44d80a92e68c668a75f27c" -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nom" version = "7.1.3" @@ -4172,9 +4087,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -4188,15 +4103,10 @@ checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.11.0", "block2 0.6.1", - "libc", - "objc2 0.6.1", - "objc2-cloud-kit", - "objc2-core-data", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image", "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", ] [[package]] @@ -4206,7 +4116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -4216,8 +4126,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -4228,8 +4137,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.11.0", - "dispatch2 0.3.0", - "objc2 0.6.1", + "dispatch2", + "objc2 0.6.4", ] [[package]] @@ -4239,8 +4148,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.11.0", - "dispatch2 0.3.0", - "objc2 0.6.1", + "dispatch2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -4251,7 +4160,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -4291,7 +4210,7 @@ dependencies = [ "bitflags 2.11.0", "block2 0.6.1", "libc", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4302,17 +4221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.1", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" -dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4335,7 +4244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.1", ] @@ -4360,19 +4269,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.1", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-security" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-core-foundation", + "objc2-foundation 0.3.1", ] [[package]] @@ -4382,8 +4281,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.1", + "block2 0.6.1", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -4395,12 +4312,10 @@ checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ "bitflags 2.11.0", "block2 0.6.1", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", - "objc2-javascript-core", - "objc2-security", ] [[package]] @@ -4549,7 +4464,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", "objc2-osa-kit", "serde", @@ -4899,7 +4814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec96245d28afd809ea0d830d0cdedb372907d5f6d9c8fac2683888f14dfd62c" dependencies = [ "cfg-if", - "indexmap 2.9.0", + "indexmap 2.14.0", "json-strip-comments", "libc", "once_cell", @@ -4995,7 +4910,7 @@ checksum = "7d55a4cd5291f307ae9138e30de09b6b3004b07001df7b70b9d9d72b87593d07" dependencies = [ "base64 0.22.1", "compact_str", - "indexmap 2.9.0", + "indexmap 2.14.0", "itoa", "memchr", "oxc_allocator", @@ -5189,7 +5104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap 2.9.0", + "indexmap 2.14.0", ] [[package]] @@ -5200,30 +5115,10 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.3", - "indexmap 2.9.0", + "indexmap 2.14.0", "serde", ] -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - [[package]] name = "phf" version = "0.11.3" @@ -5247,42 +5142,12 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", + "phf_generator 0.13.1", + "phf_shared 0.13.1", ] [[package]] @@ -5305,20 +5170,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "phf_macros" version = "0.11.3" @@ -5345,31 +5196,13 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -5378,7 +5211,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -5437,7 +5270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.9.0", + "indexmap 2.14.0", "quick-xml 0.32.0", "serde", "time", @@ -5456,6 +5289,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "pnp" version = "0.12.7" @@ -5652,12 +5498,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.95" @@ -5769,61 +5609,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.1", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.40" @@ -5877,20 +5662,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - [[package]] name = "rand" version = "0.8.5" @@ -5912,16 +5683,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -5942,15 +5703,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -5969,24 +5721,6 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -6172,7 +5906,6 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "serde", @@ -6189,26 +5922,63 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log 0.4.29", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", "web-sys", - "webpki-roots", ] [[package]] name = "rfd" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ - "ashpd", "block2 0.6.1", - "dispatch2 0.2.0", + "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log 0.4.29", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -6216,7 +5986,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6274,10 +6044,10 @@ dependencies = [ "bitflags 2.11.0", "commondir", "css-module-lexer", - "derive_more 2.1.1", + "derive_more", "dunce", "futures", - "indexmap 2.9.0", + "indexmap 2.14.0", "itertools", "itoa", "json-escape-simd", @@ -6384,7 +6154,7 @@ dependencies = [ "arcstr", "bitflags 2.11.0", "dashmap", - "derive_more 2.1.1", + "derive_more", "fast-glob", "itertools", "num-bigint", @@ -6465,7 +6235,7 @@ dependencies = [ "anyhow", "arcstr", "bitflags 2.11.0", - "derive_more 2.1.1", + "derive_more", "heck 0.5.0", "oxc", "oxc_resolver", @@ -6497,7 +6267,7 @@ dependencies = [ "async-trait", "bitflags 2.11.0", "dashmap", - "derive_more 2.1.1", + "derive_more", "oxc_index", "rolldown_common", "rolldown_debug", @@ -6636,7 +6406,7 @@ dependencies = [ "fast-glob", "form_urlencoded", "futures", - "indexmap 2.9.0", + "indexmap 2.14.0", "infer", "itoa", "memchr", @@ -6823,7 +6593,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] @@ -7028,18 +6797,19 @@ dependencies = [ [[package]] name = "selectors" -version = "0.24.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.0", "cssparser", - "derive_more 0.99.20", - "fxhash", + "derive_more", "log 0.4.29", - "phf 0.8.0", - "phf_codegen 0.8.0", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen", "precomputed-hash", + "rustc-hash 2.1.1", "servo_arc", "smallvec", ] @@ -7144,7 +6914,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.14.0", "itoa", "memchr", "ryu", @@ -7185,11 +6955,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -7214,7 +6984,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.14.0", "serde", "serde_derive", "serde_json", @@ -7240,7 +7010,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -7292,11 +7062,10 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] @@ -7401,12 +7170,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -7522,25 +7285,24 @@ checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" [[package]] name = "string_cache" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared 0.13.1", "precomputed-hash", - "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", ] @@ -7689,35 +7451,35 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" dependencies = [ "bitflags 2.11.0", "block2 0.6.1", "core-foundation 0.10.1", - "core-graphics 0.24.0", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static 1.5.0", "libc", "log 0.4.29", "ndk", - "ndk-context", "ndk-sys", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.1", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -7763,9 +7525,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.5" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" +checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405" dependencies = [ "anyhow", "bytes", @@ -7784,7 +7546,7 @@ dependencies = [ "log 0.4.29", "mime", "muda", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.1", "objc2-ui-kit", @@ -7792,7 +7554,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.3", "serde", "serde_json", "serde_repr", @@ -7815,9 +7577,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.3" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" dependencies = [ "anyhow", "cargo_toml", @@ -7831,22 +7593,21 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.5", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528" dependencies = [ "base64 0.22.1", "brotli 8.0.1", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -7864,9 +7625,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -7878,9 +7639,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" +checksum = "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee" dependencies = [ "anyhow", "glob", @@ -7889,7 +7650,6 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.5", "walkdir", ] @@ -7910,9 +7670,9 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" -version = "2.4.5" +version = "2.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73" +checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa" dependencies = [ "dunce", "plist", @@ -7931,9 +7691,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.4.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" dependencies = [ "log 0.4.29", "raw-window-handle", @@ -7949,13 +7709,15 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.4.4" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" dependencies = [ "anyhow", "dunce", "glob", + "log 0.4.29", + "objc2-foundation 0.3.1", "percent-encoding", "schemars", "serde", @@ -7965,21 +7727,21 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.17", - "toml 0.9.5", + "toml 1.1.2+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-log" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" dependencies = [ "android_logger", "byte-unit", "fern", "log 0.4.29", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", "serde", "serde_json", @@ -7993,9 +7755,9 @@ dependencies = [ [[package]] name = "tauri-plugin-opener" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -8033,9 +7795,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.3" +version = "2.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" dependencies = [ "encoding_rs", "log 0.4.29", @@ -8054,9 +7816,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.3.6" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" dependencies = [ "serde", "serde_json", @@ -8070,9 +7832,9 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" dependencies = [ "base64 0.22.1", "dirs", @@ -8084,7 +7846,8 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.13.3", + "rustls", "semver", "serde", "serde_json", @@ -8117,16 +7880,16 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", @@ -8142,17 +7905,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.3" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" +checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" dependencies = [ "gtk", "http", "jni", "log 0.4.29", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.1", "once_cell", "percent-encoding", "raw-window-handle", @@ -8169,24 +7931,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" dependencies = [ "anyhow", "brotli 8.0.1", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever", "http", "infer", "json-patch", - "kuchikiki", "log 0.4.29", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex 1.11.1", @@ -8212,7 +7974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" dependencies = [ "embed-resource", - "indexmap 2.9.0", + "indexmap 2.14.0", "toml 0.8.23", ] @@ -8231,12 +7993,11 @@ dependencies = [ [[package]] name = "tendril" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" dependencies = [ - "futf", - "mac", + "new_debug_unreachable", "utf-8", ] @@ -8427,7 +8188,6 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", - "tracing", "windows-sys 0.61.2", ] @@ -8520,15 +8280,30 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.14.0", "serde", - "serde_spanned 1.0.0", + "serde_spanned 1.1.1", "toml_datetime 0.7.0", "toml_parser", "toml_writer", "winnow 0.7.10", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -8547,13 +8322,22 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.14.0", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -8564,7 +8348,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.14.0", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -8575,7 +8359,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -8585,11 +8369,11 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 0.7.10", + "winnow 1.0.2", ] [[package]] @@ -8600,9 +8384,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" @@ -8684,20 +8468,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "bitflags 2.11.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower 0.5.2", "tower-layer", "tower-service", + "url", ] [[package]] @@ -8795,24 +8579,24 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.17", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9210,12 +8994,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9233,9 +9011,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -9243,40 +9021,24 @@ dependencies = [ "serde", "serde_json", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log 0.4.29", - "proc-macro2", - "quote", - "syn 2.0.101", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9284,22 +9046,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.101", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -9317,6 +9079,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.10" @@ -9389,22 +9164,24 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "web-time" -version = "1.1.0" +name = "web_atoms" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ - "js-sys", - "wasm-bindgen", + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -9417,7 +9194,7 @@ dependencies = [ "jni", "log 0.4.29", "ndk-context", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", "url", "web-sys", @@ -9425,9 +9202,9 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -9449,9 +9226,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -9476,15 +9253,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webview2-com" version = "0.38.0" @@ -9549,7 +9317,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -9564,7 +9332,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -10107,6 +9875,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + [[package]] name = "winreg" version = "0.10.1" @@ -10162,27 +9936,26 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wry" -version = "0.53.4" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2 0.6.1", "cookie", "crossbeam-channel", "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -10292,7 +10065,7 @@ name = "yaak-api" version = "0.1.0" dependencies = [ "log 0.4.29", - "reqwest", + "reqwest 0.12.20", "sysproxy", "thiserror 2.0.17", "yaak-common", @@ -10310,12 +10083,13 @@ dependencies = [ "log 0.4.29", "md5 0.8.0", "mime_guess", + "notify", "openssl-sys", "pretty_graphql", "r2d2", "r2d2_sqlite", "rand 0.9.1", - "reqwest", + "reqwest 0.12.20", "serde", "serde_json", "tauri", @@ -10396,7 +10170,7 @@ dependencies = [ "oxc_resolver", "predicates", "rand 0.8.5", - "reqwest", + "reqwest 0.12.20", "rolldown", "schemars", "serde", @@ -10538,7 +10312,7 @@ dependencies = [ "mime_guess", "native-tls", "regex 1.11.1", - "reqwest", + "reqwest 0.12.20", "serde", "serde_json", "thiserror 2.0.17", @@ -10560,7 +10334,7 @@ version = "0.1.0" dependencies = [ "chrono", "log 0.4.29", - "reqwest", + "reqwest 0.12.20", "serde", "serde_json", "tauri", @@ -10623,7 +10397,7 @@ dependencies = [ "md5 0.7.0", "path-slash", "rand 0.9.1", - "reqwest", + "reqwest 0.12.20", "serde", "serde_json", "sha2", @@ -10843,7 +10617,6 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", - "tokio", "tracing", "uds_windows", "windows-sys 0.60.2", @@ -10989,7 +10762,7 @@ dependencies = [ "flate2", "getrandom 0.3.3", "hmac", - "indexmap 2.9.0", + "indexmap 2.14.0", "liblzma", "memchr", "pbkdf2", @@ -11066,7 +10839,6 @@ dependencies = [ "endi", "enumflags2", "serde", - "url", "winnow 0.7.10", "zvariant_derive", "zvariant_utils", diff --git a/Cargo.toml b/Cargo.toml index 48090c3d..41bff103 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,10 +47,10 @@ schemars = { version = "0.8.22", features = ["chrono"] } serde = "1.0.228" serde_json = "1.0.145" sha2 = "0.10.9" -tauri = "2.9.5" -tauri-plugin = "2.5.2" -tauri-plugin-dialog = "2.4.2" -tauri-plugin-shell = "2.3.3" +tauri = "2.11.1" +tauri-plugin = "2.6.1" +tauri-plugin-dialog = "2.7.1" +tauri-plugin-shell = "2.3.5" thiserror = "2.0.17" tokio = "1.48.0" ts-rs = "11.1.0" diff --git a/apps/yaak-client/components/EnvironmentEditDialog.tsx b/apps/yaak-client/components/EnvironmentEditDialog.tsx index effc819d..1b6c6b49 100644 --- a/apps/yaak-client/components/EnvironmentEditDialog.tsx +++ b/apps/yaak-client/components/EnvironmentEditDialog.tsx @@ -3,7 +3,7 @@ import { duplicateModel, patchModel } from "@yaakapp-internal/models"; import type { TreeHandle, TreeNode, TreeProps } from "@yaakapp-internal/ui"; import { Banner, Icon, InlineCode, SplitLayout, Tree } from "@yaakapp-internal/ui"; import { atom, useAtomValue } from "jotai"; -import { atomFamily } from "jotai/utils"; +import { atomFamily } from "jotai-family"; import { useCallback, useLayoutEffect, useRef, useState } from "react"; import { createSubEnvironmentAndActivate } from "../commands/createEnvironment"; import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace"; diff --git a/apps/yaak-client/components/Sidebar.tsx b/apps/yaak-client/components/Sidebar.tsx index dae1347c..985dc078 100644 --- a/apps/yaak-client/components/Sidebar.tsx +++ b/apps/yaak-client/components/Sidebar.tsx @@ -1,6 +1,8 @@ import type { Extension } from "@codemirror/state"; import { Compartment } from "@codemirror/state"; import { debounce } from "@yaakapp-internal/lib"; +import { gitMutations } from "@yaakapp-internal/git"; +import type { GitStatus } from "@yaakapp-internal/git"; import type { AnyModel, Folder, @@ -23,13 +25,18 @@ import { } from "@yaakapp-internal/models"; import classNames from "classnames"; import { atom, useAtomValue } from "jotai"; -import { atomFamily, selectAtom } from "jotai/utils"; +import { atomFamily } from "jotai-family"; +import { selectAtom } from "jotai/utils"; import { memo, useCallback, useEffect, useMemo, useRef } from "react"; import { moveToWorkspace } from "../commands/moveToWorkspace"; import { openFolderSettings } from "../commands/openFolderSettings"; import { activeFolderIdAtom } from "../hooks/useActiveFolderId"; import { activeRequestIdAtom } from "../hooks/useActiveRequestId"; -import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace"; +import { + activeWorkspaceAtom, + activeWorkspaceIdAtom, + activeWorkspaceMetaAtom, +} from "../hooks/useActiveWorkspace"; import { allRequestsAtom } from "../hooks/useAllRequests"; import { getCreateDropdownItems } from "../hooks/useCreateDropdownItems"; import { getFolderActions } from "../hooks/useFolderActions"; @@ -42,7 +49,13 @@ import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest"; import { useSidebarHidden } from "../hooks/useSidebarHidden"; import { getWebsocketRequestActions } from "../hooks/useWebsocketRequestActions"; import { deepEqualAtom } from "../lib/atoms"; +import { showConfirm } from "../lib/confirm"; import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm"; +import { showDialog } from "../lib/dialog"; +import { + gitWorktreeStatusByModelIdAtom, + gitWorktreeStatusFamily, +} from "../lib/gitWorktreeStatus"; import { jotaiStore } from "../lib/jotai"; import { resolvedModelName } from "../lib/resolvedModelName"; import { isSidebarFocused } from "../lib/scopes"; @@ -68,6 +81,9 @@ import type { InputHandle } from "./core/Input"; import { Input } from "./core/Input"; import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage"; import { GitDropdown } from "./git/GitDropdown"; +import { gitCallbacks } from "./git/callbacks"; +import { FileHistoryDialog } from "./git/FileHistoryDialog"; +import { sync } from "../init/sync"; const collapsedFamily = atomFamily((treeId: string) => { const key = ["sidebar_collapsed", treeId ?? "n/a"]; @@ -375,6 +391,8 @@ function Sidebar({ className }: { className?: string }) { } const workspaces = jotaiStore.get(workspacesAtom); + const syncDir = jotaiStore.get(activeWorkspaceMetaAtom)?.settingSyncDir; + const gitItems = getGitContextMenuItems({ items, syncDir }); const onlyHttpRequests = items.every((i) => i.model === "http_request"); const requestItems = items.filter( (i) => @@ -458,8 +476,10 @@ function Sidebar({ className }: { className?: string }) { ...initialItems, { type: "separator", - hidden: initialItems.filter((v) => !v.hidden).length === 0, + hidden: initialItems.filter((v) => !v.hidden).length === 0 || gitItems.length === 0, }, + ...gitItems, + { type: "separator", hidden: gitItems.length === 0 }, { label: "Rename", leftSlot: , @@ -661,6 +681,73 @@ function Sidebar({ className }: { className?: string }) { export default Sidebar; +function getGitContextMenuItems({ + items, + syncDir, +}: { + items: SidebarModel[]; + syncDir: string | null | undefined; +}): DropdownItem[] { + if (syncDir == null) return []; + + const gitStatusEntries = items.flatMap((item) => { + const status = jotaiStore.get(gitWorktreeStatusFamily(item.id)); + return status == null || status.status === "current" ? [] : [status]; + }); + const historyItem = items.length === 1 ? items[0] : null; + const historyPath = + historyItem == null + ? null + : (jotaiStore.get(gitWorktreeStatusFamily(historyItem.id))?.relaPath ?? + syncPathForModel(historyItem)); + + return [ + { + label: "View History", + leftSlot: , + hidden: historyPath == null, + onSelect: () => { + if (historyPath == null) return; + showDialog({ + id: "git-history", + size: "lg", + title: "File History", + noPadding: true, + noScroll: true, + render: () => , + }); + }, + }, + { + label: "Restore Changes", + leftSlot: , + hidden: gitStatusEntries.length === 0, + async onSelect() { + const confirmed = await showConfirm({ + id: "git-restore-sidebar-items", + title: "Restore Changes", + description: + gitStatusEntries.length === 1 + ? "This will discard uncommitted changes for the selected item." + : `This will discard uncommitted changes for ${gitStatusEntries.length} selected items.`, + confirmText: "Restore", + color: "danger", + }); + if (!confirmed) return; + + await gitMutations(syncDir, gitCallbacks(syncDir)).restore.mutateAsync({ + relaPaths: gitStatusEntries.map((entry) => entry.relaPath), + }); + await sync({ force: true }); + }, + }, + ]; +} + +function syncPathForModel(item: SidebarModel) { + return `yaak.${item.id}.yaml`; +} + const activeIdAtom = atom((get) => { return get(activeRequestIdAtom) || get(activeFolderIdAtom); }); @@ -790,6 +877,64 @@ const sidebarTreeAtom = atom<[TreeNode, FieldDef[]] | null>((get) return [root, fields] as const; }); +const sidebarGitStatusByModelIdAtom = atom>((get) => { + const allModels = get(memoAllPotentialChildrenAtom); + const activeWorkspace = get(activeWorkspaceAtom); + const gitStatusByModelId = get(gitWorktreeStatusByModelIdAtom); + const childrenMap: Record[]> = {}; + const statusByModelId: Record = {}; + + for (const item of allModels) { + if ("folderId" in item && item.folderId == null) { + childrenMap[item.workspaceId] = childrenMap[item.workspaceId] ?? []; + childrenMap[item.workspaceId]?.push(item); + } else if ("folderId" in item && item.folderId != null) { + childrenMap[item.folderId] = childrenMap[item.folderId] ?? []; + childrenMap[item.folderId]?.push(item); + } + } + + const visit = (item: SidebarModel): GitStatus | null => { + const statuses: GitStatus[] = []; + const directStatus = gitStatusByModelId[item.id]?.status; + if (directStatus != null && directStatus !== "current") { + statuses.push(directStatus); + } + + for (const child of childrenMap[item.id] ?? []) { + const childStatus = visit(child); + if (childStatus != null) statuses.push(childStatus); + } + + const status = summarizeGitStatuses(statuses); + if (status != null) { + statusByModelId[item.id] = status; + } + return status; + }; + + if (activeWorkspace != null) { + visit(activeWorkspace); + } + + return statusByModelId; +}); + +const sidebarGitStatusFamily = atomFamily( + (modelId: string) => + selectAtom(sidebarGitStatusByModelIdAtom, (statusByModelId) => statusByModelId[modelId] ?? null), + Object.is, +); + +function summarizeGitStatuses(statuses: GitStatus[]): GitStatus | null { + if (statuses.length === 0) return null; + const firstStatus = statuses[0]; + if (firstStatus != null && statuses.every((status) => status === firstStatus)) { + return firstStatus; + } + return "modified"; +} + function getItemKey(item: SidebarModel) { const responses = jotaiStore.get(httpResponsesAtom); const latestResponse = responses.find((r) => r.requestId === item.id) ?? null; @@ -836,6 +981,7 @@ const SidebarInnerItem = memo(function SidebarInnerItem({ treeId: string; item: SidebarModel; }) { + const gitStatus = useAtomValue(sidebarGitStatusFamily(item.id)); const response = useAtomValue( useMemo( () => @@ -854,7 +1000,16 @@ const SidebarInnerItem = memo(function SidebarInnerItem({ return (
-
{resolvedModelName(item)}
+
+ {resolvedModelName(item)} +
{response != null && (
{response.state !== "closed" ? ( diff --git a/apps/yaak-client/components/Workspace.tsx b/apps/yaak-client/components/Workspace.tsx index 9557c9f5..0ce50373 100644 --- a/apps/yaak-client/components/Workspace.tsx +++ b/apps/yaak-client/components/Workspace.tsx @@ -128,7 +128,7 @@ export function Workspace() { ); return ( -
+
{header} (0); - const timeout = useRef(undefined); + const timeout = useRef>(undefined); // Calculate the duration of the response for use when the response hasn't finished yet useEffect(() => { diff --git a/apps/yaak-client/components/core/Tooltip.tsx b/apps/yaak-client/components/core/Tooltip.tsx index 94d71a50..beccd289 100644 --- a/apps/yaak-client/components/core/Tooltip.tsx +++ b/apps/yaak-client/components/core/Tooltip.tsx @@ -31,7 +31,7 @@ export function Tooltip({ children, className, content, tabIndex, size = "md" }: const [openState, setOpenState] = useState(null); const triggerRef = useRef(null); const tooltipRef = useRef(null); - const showTimeout = useRef(undefined); + const showTimeout = useRef>(undefined); const handleOpenImmediate = () => { if (triggerRef.current == null || tooltipRef.current == null) return; diff --git a/apps/yaak-client/components/git/FileHistoryDialog.tsx b/apps/yaak-client/components/git/FileHistoryDialog.tsx new file mode 100644 index 00000000..90c416e0 --- /dev/null +++ b/apps/yaak-client/components/git/FileHistoryDialog.tsx @@ -0,0 +1,131 @@ +import { useGitFileDiffForCommit, useGitLog, useGitMutations } from "@yaakapp-internal/git"; +import type { GitCommit } from "@yaakapp-internal/git"; +import { InlineCode, SplitLayout } from "@yaakapp-internal/ui"; +import classNames from "classnames"; +import { formatDistanceToNowStrict } from "date-fns"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { sync } from "../../init/sync"; +import { showConfirm } from "../../lib/confirm"; +import { EmptyStateText } from "../EmptyStateText"; +import { Button } from "../core/Button"; +import { DiffViewer } from "../core/Editor/DiffViewer"; +import { useGitCallbacks } from "./callbacks"; + +export function FileHistoryDialog({ dir, relaPath }: { dir: string; relaPath: string }) { + const callbacks = useGitCallbacks(dir); + const { restoreFileFromCommit } = useGitMutations(dir, callbacks); + const log = useGitLog(dir, undefined, relaPath); + const commits = log.data ?? []; + const [selectedOid, setSelectedOid] = useState(null); + const selectedCommit = useMemo( + () => commits.find((commit) => commit.oid === selectedOid) ?? null, + [commits, selectedOid], + ); + const diff = useGitFileDiffForCommit(dir, relaPath, selectedCommit?.oid); + + useEffect(() => { + if (commits.length === 0) { + setSelectedOid(null); + } else if (selectedOid == null || !commits.some((commit) => commit.oid === selectedOid)) { + setSelectedOid(commits[0]?.oid ?? null); + } + }, [commits, selectedOid]); + + const handleRestoreCommit = useCallback( + async (commit: GitCommit) => { + const confirmed = await showConfirm({ + id: "git-restore-file-history-entry", + title: "Restore File", + description: "This will restore the file to the selected commit.", + confirmText: "Restore", + color: "warning", + }); + if (!confirmed) return; + + await restoreFileFromCommit.mutateAsync({ commitOid: commit.oid, relaPath }); + await sync({ force: true }); + }, + [relaPath, restoreFileFromCommit], + ); + + if (commits.length === 0 && !log.isLoading) { + return No history for this file; + } + + return ( +
+ ( +
+
+ {commits.map((commit) => ( + setSelectedOid(commit.oid)} + /> + ))} +
+
+ )} + secondSlot={({ style }) => ( +
+ {selectedCommit == null ? ( + Select a commit to view diff + ) : ( +
+
+
{selectedCommit.message || "No message"}
+ +
+ +
+ )} +
+ )} + /> +
+ ); +} + +function CommitListItem({ + commit, + selected, + onSelect, +}: { + commit: GitCommit; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} diff --git a/apps/yaak-client/components/git/GitCommitDialog.tsx b/apps/yaak-client/components/git/GitCommitDialog.tsx index 79765257..fd1b65cf 100644 --- a/apps/yaak-client/components/git/GitCommitDialog.tsx +++ b/apps/yaak-client/components/git/GitCommitDialog.tsx @@ -8,12 +8,14 @@ import type { WebsocketRequest, Workspace, } from "@yaakapp-internal/models"; -import { Banner, HStack, Icon, InlineCode, SplitLayout } from "@yaakapp-internal/ui"; +import { Banner, HStack, Icon, IconButton, InlineCode, SplitLayout } from "@yaakapp-internal/ui"; import classNames from "classnames"; import { useCallback, useMemo, useState } from "react"; import { modelToYaml } from "../../lib/diffYaml"; import { resolvedModelName } from "../../lib/resolvedModelName"; +import { showConfirm } from "../../lib/confirm"; import { showErrorToast } from "../../lib/toast"; +import { sync } from "../../init/sync"; import { Button } from "../core/Button"; import type { CheckboxProps } from "../core/Checkbox"; import { Checkbox } from "../core/Checkbox"; @@ -21,7 +23,7 @@ import { DiffViewer } from "../core/Editor/DiffViewer"; import { Input } from "../core/Input"; import { Separator } from "../core/Separator"; import { EmptyStateText } from "../EmptyStateText"; -import { gitCallbacks } from "./callbacks"; +import { useGitCallbacks } from "./callbacks"; import { handlePushResult } from "./git-util"; interface Props { @@ -38,9 +40,10 @@ interface CommitTreeNode { } export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { - const [{ status }, { commit, commitAndPush, add, unstage }] = useGit( + const callbacks = useGitCallbacks(syncDir); + const [{ status }, { commit, commitAndPush, add, unstage, restore }] = useGit( syncDir, - gitCallbacks(syncDir), + callbacks, ); const [isPushing, setIsPushing] = useState(false); const [commitError, setCommitError] = useState(null); @@ -165,6 +168,24 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { [selectedEntry], ); + const handleDiscardChanges = useCallback( + async (entry: GitStatusEntry) => { + const confirmed = await showConfirm({ + id: "git-restore-commit-entry", + title: "Discard Changes", + description: "Do you really want to discard uncommitted changes for the selected item?", + confirmText: "Discard", + color: "danger", + }); + if (!confirmed) return; + + await restore.mutateAsync({ relaPaths: [entry.relaPath] }); + await sync({ force: true }); + setSelectedEntry(null); + }, + [restore], + ); + if (tree == null) { return null; } @@ -259,7 +280,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { secondSlot={({ style }) => (
{selectedEntry ? ( - + ) : ( Select a change to view diff )} @@ -466,16 +487,35 @@ function isNodeRelevant(node: CommitTreeNode): boolean { return node.children.some((c) => isNodeRelevant(c)); } -function DiffPanel({ entry }: { entry: GitStatusEntry }) { +function DiffPanel({ + entry, + onDiscardChanges, +}: { + entry: GitStatusEntry; + onDiscardChanges: (entry: GitStatusEntry) => void | Promise; +}) { const prevYaml = modelToYaml(entry.prev); const nextYaml = modelToYaml(entry.next); return (
-
- {resolvedModelName(entry.next ?? entry.prev)} ({entry.status}) +
+
+ {resolvedModelName(entry.next ?? entry.prev)} ({entry.status}) +
+
- +
); } diff --git a/apps/yaak-client/components/git/GitDropdown.tsx b/apps/yaak-client/components/git/GitDropdown.tsx index 49a53203..a88661dc 100644 --- a/apps/yaak-client/components/git/GitDropdown.tsx +++ b/apps/yaak-client/components/git/GitDropdown.tsx @@ -1,9 +1,9 @@ -import { useGit } from "@yaakapp-internal/git"; +import { useGitBranchInfo, useGitMutations } from "@yaakapp-internal/git"; import type { WorkspaceMeta } from "@yaakapp-internal/models"; import classNames from "classnames"; import { useAtomValue } from "jotai"; import type { HTMLAttributes } from "react"; -import { forwardRef } from "react"; +import { forwardRef, useCallback, useMemo } from "react"; import { openWorkspaceSettings } from "../../commands/openWorkspaceSettings"; import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from "../../hooks/useActiveWorkspace"; import { useKeyValue } from "../../hooks/useKeyValue"; @@ -12,17 +12,20 @@ import { sync } from "../../init/sync"; import { showConfirm, showConfirmDelete } from "../../lib/confirm"; import { fireAndForget } from "../../lib/fireAndForget"; import { showDialog } from "../../lib/dialog"; +import { gitWorktreeStatusAtom } from "../../lib/gitWorktreeStatus"; import { showPrompt } from "../../lib/prompt"; import { showErrorToast, showToast } from "../../lib/toast"; import type { DropdownItem } from "../core/Dropdown"; import { Dropdown } from "../core/Dropdown"; import { Banner, Icon, InlineCode } from "@yaakapp-internal/ui"; -import { gitCallbacks } from "./callbacks"; +import { useGitCallbacks } from "./callbacks"; import { GitCommitDialog } from "./GitCommitDialog"; import { GitRemotesDialog } from "./GitRemotesDialog"; import { handlePullResult, handlePushResult } from "./git-util"; import { HistoryDialog } from "./HistoryDialog"; +const EMPTY_BRANCHES: string[] = []; + export function GitDropdown() { const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom); if (workspaceMeta == null) return null; @@ -36,469 +39,493 @@ export function GitDropdown() { function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { const workspace = useAtomValue(activeWorkspaceAtom); + const worktreeStatus = useAtomValue(gitWorktreeStatusAtom); const [refreshKey, regenerateKey] = useRandomKey(); - const [ - { status, log }, - { - createBranch, - deleteBranch, - deleteRemoteBranch, - renameBranch, - mergeBranch, - push, - pull, - checkout, - resetChanges, - init, - }, - ] = useGit(syncDir, gitCallbacks(syncDir), refreshKey); + const branchInfo = useGitBranchInfo(syncDir, refreshKey); + const callbacks = useGitCallbacks(syncDir); + const { + createBranch, + deleteBranch, + deleteRemoteBranch, + renameBranch, + mergeBranch, + push, + pull, + checkout, + resetChanges, + init, + } = useGitMutations(syncDir, callbacks); - const localBranches = status.data?.localBranches ?? []; - const remoteBranches = status.data?.remoteBranches ?? []; - const remoteOnlyBranches = remoteBranches.filter( - (b) => !localBranches.includes(b.replace(/^origin\//, "")), + const localBranches = branchInfo.data?.localBranches ?? EMPTY_BRANCHES; + const remoteBranches = branchInfo.data?.remoteBranches ?? EMPTY_BRANCHES; + const remoteOnlyBranches = useMemo( + () => remoteBranches.filter((b) => !localBranches.includes(b.replace(/^origin\//, ""))), + [localBranches, remoteBranches], ); + const currentBranch = branchInfo.data?.headRefShorthand; + const hasChanges = worktreeStatus?.entries.some((e) => e.status !== "current") ?? false; + const ahead = branchInfo.data?.ahead ?? 0; + const behind = branchInfo.data?.behind ?? 0; + const initRepo = useCallback(() => { + init.mutate(); + }, [init]); + + const items: DropdownItem[] = useMemo(() => { + if (workspace == null || branchInfo.data == null) return []; + + const tryCheckout = (branch: string, force: boolean) => { + checkout.mutate( + { branch, force }, + { + disableToastError: true, + async onError(err) { + if (!force) { + // Checkout failed so ask user if they want to force it + const forceCheckout = await showConfirm({ + id: "git-force-checkout", + title: "Conflicts Detected", + description: + "Your branch has conflicts. Either make a commit or force checkout to discard changes.", + confirmText: "Force Checkout", + color: "warning", + }); + if (forceCheckout) { + tryCheckout(branch, true); + } + } else { + // Checkout failed + showErrorToast({ + id: "git-checkout-error", + title: "Error checking out branch", + message: String(err), + }); + } + }, + async onSuccess(branchName) { + showToast({ + id: "git-checkout-success", + message: ( + <> + Switched branch {branchName} + + ), + color: "success", + }); + await sync({ force: true }); + }, + }, + ); + }; + + return [ + { + label: "View History...", + leftSlot: , + onSelect: async () => { + showDialog({ + id: "git-history", + size: "md", + title: "Commit History", + noPadding: true, + render: () => , + }); + }, + }, + { + label: "Manage Remotes...", + leftSlot: , + onSelect: () => GitRemotesDialog.show(syncDir), + }, + { type: "separator" }, + { + label: "New Branch...", + leftSlot: , + async onSelect() { + const name = await showPrompt({ + id: "git-branch-name", + title: "Create Branch", + label: "Branch Name", + }); + if (!name) return; + + await createBranch.mutateAsync( + { branch: name }, + { + disableToastError: true, + onError: (err) => { + showErrorToast({ + id: "git-branch-error", + title: "Error creating branch", + message: String(err), + }); + }, + }, + ); + tryCheckout(name, false); + }, + }, + { type: "separator" }, + { + label: "Push", + leftSlot: , + waitForOnSelect: true, + async onSelect() { + await push.mutateAsync(undefined, { + disableToastError: true, + onSuccess: handlePushResult, + onError(err) { + showErrorToast({ + id: "git-push-error", + title: "Error pushing changes", + message: String(err), + }); + }, + }); + }, + }, + { + label: "Pull", + leftSlot: , + waitForOnSelect: true, + async onSelect() { + await pull.mutateAsync(undefined, { + disableToastError: true, + onSuccess: handlePullResult, + onError(err) { + showErrorToast({ + id: "git-pull-error", + title: "Error pulling changes", + message: String(err), + }); + }, + }); + }, + }, + { + label: "Commit...", + + leftSlot: , + onSelect() { + showDialog({ + id: "commit", + title: "Commit Changes", + size: "full", + noPadding: true, + render: ({ hide }) => ( + + ), + }); + }, + }, + { + label: "Reset Changes", + hidden: !hasChanges, + leftSlot: , + color: "danger", + async onSelect() { + const confirmed = await showConfirm({ + id: "git-reset-changes", + title: "Reset Changes", + description: "This will discard all uncommitted changes. This cannot be undone.", + confirmText: "Reset", + color: "danger", + }); + if (!confirmed) return; + + await resetChanges.mutateAsync(undefined, { + disableToastError: true, + onSuccess() { + showToast({ + id: "git-reset-success", + message: "Changes have been reset", + color: "success", + }); + fireAndForget(sync({ force: true })); + }, + onError(err) { + showErrorToast({ + id: "git-reset-error", + title: "Error resetting changes", + message: String(err), + }); + }, + }); + }, + }, + { type: "separator", label: "Branches", hidden: localBranches.length < 1 }, + ...localBranches.map((branch) => { + const isCurrent = currentBranch === branch; + return { + label: branch, + leftSlot: , + submenuOpenOnClick: true, + submenu: [ + { + label: "Checkout", + hidden: isCurrent, + onSelect: () => tryCheckout(branch, false), + }, + { + label: ( + <> + Merge into {currentBranch} + + ), + hidden: isCurrent, + async onSelect() { + await mergeBranch.mutateAsync( + { branch }, + { + disableToastError: true, + onSuccess() { + showToast({ + id: "git-merged-branch", + message: ( + <> + Merged {branch} into{" "} + {currentBranch} + + ), + }); + fireAndForget(sync({ force: true })); + }, + onError(err) { + showErrorToast({ + id: "git-merged-branch-error", + title: "Error merging branch", + message: String(err), + }); + }, + }, + ); + }, + }, + { + label: "New Branch...", + async onSelect() { + const name = await showPrompt({ + id: "git-new-branch-from", + title: "New Branch", + description: ( + <> + Create a new branch from {branch} + + ), + label: "Branch Name", + }); + if (!name) return; + + await createBranch.mutateAsync( + { branch: name, base: branch }, + { + disableToastError: true, + onError: (err) => { + showErrorToast({ + id: "git-branch-error", + title: "Error creating branch", + message: String(err), + }); + }, + }, + ); + tryCheckout(name, false); + }, + }, + { + label: "Rename...", + async onSelect() { + const newName = await showPrompt({ + id: "git-rename-branch", + title: "Rename Branch", + label: "New Branch Name", + defaultValue: branch, + }); + if (!newName || newName === branch) return; + + await renameBranch.mutateAsync( + { oldName: branch, newName }, + { + disableToastError: true, + onSuccess() { + showToast({ + id: "git-rename-branch-success", + message: ( + <> + Renamed {branch} to{" "} + {newName} + + ), + color: "success", + }); + }, + onError(err) { + showErrorToast({ + id: "git-rename-branch-error", + title: "Error renaming branch", + message: String(err), + }); + }, + }, + ); + }, + }, + { type: "separator", hidden: isCurrent }, + { + label: "Delete", + color: "danger", + hidden: isCurrent, + onSelect: async () => { + const confirmed = await showConfirmDelete({ + id: "git-delete-branch", + title: "Delete Branch", + description: ( + <> + Permanently delete {branch}? + + ), + }); + if (!confirmed) { + return; + } + + const result = await deleteBranch.mutateAsync( + { branch }, + { + disableToastError: true, + onError(err) { + showErrorToast({ + id: "git-delete-branch-error", + title: "Error deleting branch", + message: String(err), + }); + }, + }, + ); + + if (result.type === "not_fully_merged") { + const confirmed = await showConfirm({ + id: "force-branch-delete", + title: "Branch not fully merged", + description: ( + <> +

+ Branch {branch} is not fully merged. +

+

Do you want to delete it anyway?

+ + ), + }); + if (confirmed) { + await deleteBranch.mutateAsync( + { branch, force: true }, + { + disableToastError: true, + onError(err) { + showErrorToast({ + id: "git-force-delete-branch-error", + title: "Error force deleting branch", + message: String(err), + }); + }, + }, + ); + } + } + }, + }, + ], + } satisfies DropdownItem; + }), + ...remoteOnlyBranches.map((branch) => { + const isCurrent = currentBranch === branch; + return { + label: branch, + leftSlot: , + submenuOpenOnClick: true, + submenu: [ + { + label: "Checkout", + hidden: isCurrent, + onSelect: () => tryCheckout(branch, false), + }, + { + label: "Delete", + color: "danger", + async onSelect() { + const confirmed = await showConfirmDelete({ + id: "git-delete-remote-branch", + title: "Delete Remote Branch", + description: ( + <> + Permanently delete {branch} from the remote? + + ), + }); + if (!confirmed) return; + + await deleteRemoteBranch.mutateAsync( + { branch }, + { + disableToastError: true, + onSuccess() { + showToast({ + id: "git-delete-remote-branch-success", + message: ( + <> + Deleted remote branch {branch} + + ), + color: "success", + }); + }, + onError(err) { + showErrorToast({ + id: "git-delete-remote-branch-error", + title: "Error deleting remote branch", + message: String(err), + }); + }, + }, + ); + }, + }, + ], + } satisfies DropdownItem; + }), + ]; + }, [ + branchInfo.data, + checkout, + createBranch, + currentBranch, + deleteBranch, + deleteRemoteBranch, + hasChanges, + localBranches, + mergeBranch, + pull, + push, + remoteOnlyBranches, + renameBranch, + resetChanges, + syncDir, + workspace, + ]); + if (workspace == null) { return null; } - const noRepo = status.error?.includes("not found"); + const noRepo = branchInfo.error?.includes("not found"); if (noRepo) { - return ; + return ; } // Still loading - if (status.data == null) { + if (branchInfo.data == null) { return null; } - const currentBranch = status.data.headRefShorthand; - const hasChanges = status.data.entries.some((e) => e.status !== "current"); - const _hasRemotes = (status.data.origins ?? []).length > 0; - const { ahead, behind } = status.data; - - const tryCheckout = (branch: string, force: boolean) => { - checkout.mutate( - { branch, force }, - { - disableToastError: true, - async onError(err) { - if (!force) { - // Checkout failed so ask user if they want to force it - const forceCheckout = await showConfirm({ - id: "git-force-checkout", - title: "Conflicts Detected", - description: - "Your branch has conflicts. Either make a commit or force checkout to discard changes.", - confirmText: "Force Checkout", - color: "warning", - }); - if (forceCheckout) { - tryCheckout(branch, true); - } - } else { - // Checkout failed - showErrorToast({ - id: "git-checkout-error", - title: "Error checking out branch", - message: String(err), - }); - } - }, - async onSuccess(branchName) { - showToast({ - id: "git-checkout-success", - message: ( - <> - Switched branch {branchName} - - ), - color: "success", - }); - await sync({ force: true }); - }, - }, - ); - }; - - const items: DropdownItem[] = [ - { - label: "View History...", - hidden: (log.data ?? []).length === 0, - leftSlot: , - onSelect: async () => { - showDialog({ - id: "git-history", - size: "md", - title: "Commit History", - noPadding: true, - render: () => , - }); - }, - }, - { - label: "Manage Remotes...", - leftSlot: , - onSelect: () => GitRemotesDialog.show(syncDir), - }, - { type: "separator" }, - { - label: "New Branch...", - leftSlot: , - async onSelect() { - const name = await showPrompt({ - id: "git-branch-name", - title: "Create Branch", - label: "Branch Name", - }); - if (!name) return; - - await createBranch.mutateAsync( - { branch: name }, - { - disableToastError: true, - onError: (err) => { - showErrorToast({ - id: "git-branch-error", - title: "Error creating branch", - message: String(err), - }); - }, - }, - ); - tryCheckout(name, false); - }, - }, - { type: "separator" }, - { - label: "Push", - leftSlot: , - waitForOnSelect: true, - async onSelect() { - await push.mutateAsync(undefined, { - disableToastError: true, - onSuccess: handlePushResult, - onError(err) { - showErrorToast({ - id: "git-push-error", - title: "Error pushing changes", - message: String(err), - }); - }, - }); - }, - }, - { - label: "Pull", - leftSlot: , - waitForOnSelect: true, - async onSelect() { - await pull.mutateAsync(undefined, { - disableToastError: true, - onSuccess: handlePullResult, - onError(err) { - showErrorToast({ - id: "git-pull-error", - title: "Error pulling changes", - message: String(err), - }); - }, - }); - }, - }, - { - label: "Commit...", - - leftSlot: , - onSelect() { - showDialog({ - id: "commit", - title: "Commit Changes", - size: "full", - noPadding: true, - render: ({ hide }) => ( - - ), - }); - }, - }, - { - label: "Reset Changes", - hidden: !hasChanges, - leftSlot: , - color: "danger", - async onSelect() { - const confirmed = await showConfirm({ - id: "git-reset-changes", - title: "Reset Changes", - description: "This will discard all uncommitted changes. This cannot be undone.", - confirmText: "Reset", - color: "danger", - }); - if (!confirmed) return; - - await resetChanges.mutateAsync(undefined, { - disableToastError: true, - onSuccess() { - showToast({ - id: "git-reset-success", - message: "Changes have been reset", - color: "success", - }); - fireAndForget(sync({ force: true })); - }, - onError(err) { - showErrorToast({ - id: "git-reset-error", - title: "Error resetting changes", - message: String(err), - }); - }, - }); - }, - }, - { type: "separator", label: "Branches", hidden: localBranches.length < 1 }, - ...localBranches.map((branch) => { - const isCurrent = currentBranch === branch; - return { - label: branch, - leftSlot: , - submenuOpenOnClick: true, - submenu: [ - { - label: "Checkout", - hidden: isCurrent, - onSelect: () => tryCheckout(branch, false), - }, - { - label: ( - <> - Merge into {currentBranch} - - ), - hidden: isCurrent, - async onSelect() { - await mergeBranch.mutateAsync( - { branch }, - { - disableToastError: true, - onSuccess() { - showToast({ - id: "git-merged-branch", - message: ( - <> - Merged {branch} into{" "} - {currentBranch} - - ), - }); - fireAndForget(sync({ force: true })); - }, - onError(err) { - showErrorToast({ - id: "git-merged-branch-error", - title: "Error merging branch", - message: String(err), - }); - }, - }, - ); - }, - }, - { - label: "New Branch...", - async onSelect() { - const name = await showPrompt({ - id: "git-new-branch-from", - title: "New Branch", - description: ( - <> - Create a new branch from {branch} - - ), - label: "Branch Name", - }); - if (!name) return; - - await createBranch.mutateAsync( - { branch: name, base: branch }, - { - disableToastError: true, - onError: (err) => { - showErrorToast({ - id: "git-branch-error", - title: "Error creating branch", - message: String(err), - }); - }, - }, - ); - tryCheckout(name, false); - }, - }, - { - label: "Rename...", - async onSelect() { - const newName = await showPrompt({ - id: "git-rename-branch", - title: "Rename Branch", - label: "New Branch Name", - defaultValue: branch, - }); - if (!newName || newName === branch) return; - - await renameBranch.mutateAsync( - { oldName: branch, newName }, - { - disableToastError: true, - onSuccess() { - showToast({ - id: "git-rename-branch-success", - message: ( - <> - Renamed {branch} to{" "} - {newName} - - ), - color: "success", - }); - }, - onError(err) { - showErrorToast({ - id: "git-rename-branch-error", - title: "Error renaming branch", - message: String(err), - }); - }, - }, - ); - }, - }, - { type: "separator", hidden: isCurrent }, - { - label: "Delete", - color: "danger", - hidden: isCurrent, - onSelect: async () => { - const confirmed = await showConfirmDelete({ - id: "git-delete-branch", - title: "Delete Branch", - description: ( - <> - Permanently delete {branch}? - - ), - }); - if (!confirmed) { - return; - } - - const result = await deleteBranch.mutateAsync( - { branch }, - { - disableToastError: true, - onError(err) { - showErrorToast({ - id: "git-delete-branch-error", - title: "Error deleting branch", - message: String(err), - }); - }, - }, - ); - - if (result.type === "not_fully_merged") { - const confirmed = await showConfirm({ - id: "force-branch-delete", - title: "Branch not fully merged", - description: ( - <> -

- Branch {branch} is not fully merged. -

-

Do you want to delete it anyway?

- - ), - }); - if (confirmed) { - await deleteBranch.mutateAsync( - { branch, force: true }, - { - disableToastError: true, - onError(err) { - showErrorToast({ - id: "git-force-delete-branch-error", - title: "Error force deleting branch", - message: String(err), - }); - }, - }, - ); - } - } - }, - }, - ], - } satisfies DropdownItem; - }), - ...remoteOnlyBranches.map((branch) => { - const isCurrent = currentBranch === branch; - return { - label: branch, - leftSlot: , - submenuOpenOnClick: true, - submenu: [ - { - label: "Checkout", - hidden: isCurrent, - onSelect: () => tryCheckout(branch, false), - }, - { - label: "Delete", - color: "danger", - async onSelect() { - const confirmed = await showConfirmDelete({ - id: "git-delete-remote-branch", - title: "Delete Remote Branch", - description: ( - <> - Permanently delete {branch} from the remote? - - ), - }); - if (!confirmed) return; - - await deleteRemoteBranch.mutateAsync( - { branch }, - { - disableToastError: true, - onSuccess() { - showToast({ - id: "git-delete-remote-branch-success", - message: ( - <> - Deleted remote branch {branch} - - ), - color: "success", - }); - }, - onError(err) { - showErrorToast({ - id: "git-delete-remote-branch-error", - title: "Error deleting remote branch", - message: String(err), - }); - }, - }, - ); - }, - }, - ], - } satisfies DropdownItem; - }), - ]; - return ( diff --git a/apps/yaak-client/components/git/GitRemotesDialog.tsx b/apps/yaak-client/components/git/GitRemotesDialog.tsx index f72b546a..8a9be720 100644 --- a/apps/yaak-client/components/git/GitRemotesDialog.tsx +++ b/apps/yaak-client/components/git/GitRemotesDialog.tsx @@ -10,7 +10,7 @@ import { TableHeaderCell, TableRow, } from "@yaakapp-internal/ui"; -import { gitCallbacks } from "./callbacks"; +import { useGitCallbacks } from "./callbacks"; import { addGitRemote } from "./showAddRemoteDialog"; interface Props { @@ -19,7 +19,8 @@ interface Props { } export function GitRemotesDialog({ dir }: Props) { - const [{ remotes }, { rmRemote }] = useGit(dir, gitCallbacks(dir)); + const callbacks = useGitCallbacks(dir); + const [{ remotes }, { rmRemote }] = useGit(dir, callbacks); return ( diff --git a/apps/yaak-client/components/git/HistoryDialog.tsx b/apps/yaak-client/components/git/HistoryDialog.tsx index b1f78c0f..90324022 100644 --- a/apps/yaak-client/components/git/HistoryDialog.tsx +++ b/apps/yaak-client/components/git/HistoryDialog.tsx @@ -1,4 +1,4 @@ -import type { GitCommit } from "@yaakapp-internal/git"; +import { useGitLog } from "@yaakapp-internal/git"; import { formatDistanceToNowStrict } from "date-fns"; import { Table, @@ -10,11 +10,9 @@ import { TruncatedWideTableCell, } from "@yaakapp-internal/ui"; -interface Props { - log: GitCommit[]; -} +export function HistoryDialog({ dir }: { dir: string }) { + const log = useGitLog(dir); -export function HistoryDialog({ log }: Props) { return (
@@ -26,8 +24,8 @@ export function HistoryDialog({ log }: Props) { - {log.map((l) => ( - + {(log.data ?? []).map((l) => ( + {l.message || No message} diff --git a/apps/yaak-client/components/git/callbacks.tsx b/apps/yaak-client/components/git/callbacks.tsx index 3629f33f..b788c67c 100644 --- a/apps/yaak-client/components/git/callbacks.tsx +++ b/apps/yaak-client/components/git/callbacks.tsx @@ -1,4 +1,5 @@ import type { GitCallbacks } from "@yaakapp-internal/git"; +import { useMemo } from "react"; import { sync } from "../../init/sync"; import { promptCredentials } from "./credentials"; import { promptDivergedStrategy } from "./diverged"; @@ -24,3 +25,7 @@ export function gitCallbacks(dir: string): GitCallbacks { forceSync: () => sync({ force: true }), }; } + +export function useGitCallbacks(dir: string): GitCallbacks { + return useMemo(() => gitCallbacks(dir), [dir]); +} diff --git a/apps/yaak-client/init/git.ts b/apps/yaak-client/init/git.ts new file mode 100644 index 00000000..7267691f --- /dev/null +++ b/apps/yaak-client/init/git.ts @@ -0,0 +1,38 @@ +import { watchGitWorktreeStatus, type GitWorktreeStatusEntry } from "@yaakapp-internal/git"; +import { activeWorkspaceMetaAtom } from "../hooks/useActiveWorkspace"; +import { gitWorktreeStatusAtom, gitWorktreeStatusByModelIdAtom } from "../lib/gitWorktreeStatus"; +import { jotaiStore } from "../lib/jotai"; + +export function initGit() { + let watchedDir: string | null = null; + let unwatch: null | ReturnType = null; + + const watchActiveWorkspace = () => { + const syncDir = jotaiStore.get(activeWorkspaceMetaAtom)?.settingSyncDir ?? null; + if (syncDir === watchedDir) return; + + void unwatch?.(); + unwatch = null; + watchedDir = syncDir; + jotaiStore.set(gitWorktreeStatusAtom, null); + jotaiStore.set(gitWorktreeStatusByModelIdAtom, {}); + + if (syncDir == null) return; + + unwatch = watchGitWorktreeStatus(syncDir, (status) => { + if (syncDir !== watchedDir) return; + + jotaiStore.set(gitWorktreeStatusAtom, status); + + const statusByModelId: Record = {}; + for (const entry of status.entries) { + if (entry.modelId == null) continue; + statusByModelId[entry.modelId] = entry; + } + jotaiStore.set(gitWorktreeStatusByModelIdAtom, statusByModelId); + }); + }; + + watchActiveWorkspace(); + jotaiStore.sub(activeWorkspaceMetaAtom, watchActiveWorkspace); +} diff --git a/apps/yaak-client/init/sync.ts b/apps/yaak-client/init/sync.ts index bc856a2d..61412711 100644 --- a/apps/yaak-client/init/sync.ts +++ b/apps/yaak-client/init/sync.ts @@ -1,4 +1,4 @@ -import { debounce } from "@yaakapp-internal/lib"; +import { debounce, eagerDebounceAsync } from "@yaakapp-internal/lib"; import type { AnyModel, ModelPayload } from "@yaakapp-internal/models"; import { watchWorkspaceFiles } from "@yaakapp-internal/sync"; import { syncWorkspace } from "../commands/commands"; @@ -25,9 +25,8 @@ export async function sync({ force }: { force?: boolean } = {}) { }); } -const debouncedSync = debounce(async () => { - await sync(); -}, 1000); +const syncAfterFileChange = debounce(sync, 1000); +const syncAfterModelWrite = eagerDebounceAsync(sync, 1000); /** * Subscribe to model change events. Since we check the workspace ID on sync, we can @@ -35,7 +34,7 @@ const debouncedSync = debounce(async () => { */ function initModelListeners() { listenToTauriEvent("model_write", (p) => { - if (isModelRelevant(p.payload.model)) debouncedSync(); + if (isModelRelevant(p.payload.model)) syncAfterModelWrite(); }); } @@ -50,11 +49,11 @@ function initFileChangeListeners() { await unsub?.(); // Unsub to previous const workspaceMeta = jotaiStore.get(activeWorkspaceMetaAtom); if (workspaceMeta == null || workspaceMeta.settingSyncDir == null) return; - debouncedSync(); // Perform an initial sync when switching workspace + syncAfterFileChange(); // Perform an initial sync when switching workspace unsub = watchWorkspaceFiles( workspaceMeta.workspaceId, workspaceMeta.settingSyncDir, - debouncedSync, + syncAfterFileChange, ); }); } diff --git a/apps/yaak-client/lib/diffYaml.ts b/apps/yaak-client/lib/diffYaml.ts index 248a9cf4..42a12023 100644 --- a/apps/yaak-client/lib/diffYaml.ts +++ b/apps/yaak-client/lib/diffYaml.ts @@ -2,8 +2,7 @@ import type { SyncModel } from "@yaakapp-internal/git"; import { stringify } from "yaml"; /** - * Convert a SyncModel to a clean YAML string for diffing. - * Removes noisy fields like updatedAt that change on every edit. + * Convert a SyncModel to a YAML string for diffing. */ export function modelToYaml(model: SyncModel | null): string { if (!model) return ""; diff --git a/apps/yaak-client/lib/gitWorktreeStatus.ts b/apps/yaak-client/lib/gitWorktreeStatus.ts new file mode 100644 index 00000000..0829e750 --- /dev/null +++ b/apps/yaak-client/lib/gitWorktreeStatus.ts @@ -0,0 +1,22 @@ +import type { GitWorktreeStatus, GitWorktreeStatusEntry } from "@yaakapp-internal/git"; +import { atom } from "jotai"; +import { atomFamily } from "jotai-family"; +import { selectAtom } from "jotai/utils"; + +export const gitWorktreeStatusAtom = atom(null); + +export const gitWorktreeStatusByModelIdAtom = atom>({}); + +export const gitWorktreeStatusFamily = atomFamily( + (modelId: string) => + selectAtom( + gitWorktreeStatusByModelIdAtom, + (statusByModelId) => statusByModelId[modelId] ?? null, + (a, b) => + a?.relaPath === b?.relaPath && + a?.status === b?.status && + a?.staged === b?.staged && + a?.modelId === b?.modelId, + ), + Object.is, +); diff --git a/apps/yaak-client/main.tsx b/apps/yaak-client/main.tsx index 80233dbd..5c4e073c 100644 --- a/apps/yaak-client/main.tsx +++ b/apps/yaak-client/main.tsx @@ -5,6 +5,7 @@ import { changeModelStoreWorkspace, initModelStore } from "@yaakapp-internal/mod import { setPlatformOnDocument } from "@yaakapp-internal/theme"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { initGit } from "./init/git"; import { initSync } from "./init/sync"; import { initGlobalListeners } from "./lib/initGlobalListeners"; import { jotaiStore } from "./lib/jotai"; @@ -31,6 +32,7 @@ window.addEventListener("keydown", (e) => { }); // Initialize a bunch of watchers +initGit(); initSync(); initModelStore(jotaiStore); initGlobalListeners(); diff --git a/apps/yaak-client/package.json b/apps/yaak-client/package.json index 0f22b67e..38cd1d7f 100644 --- a/apps/yaak-client/package.json +++ b/apps/yaak-client/package.json @@ -31,14 +31,14 @@ "@tanstack/react-query": "^5.90.5", "@tanstack/react-router": "^1.133.13", "@tanstack/react-virtual": "^3.13.12", - "@tauri-apps/api": "^2.9.1", + "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", - "@tauri-apps/plugin-dialog": "^2.4.2", - "@tauri-apps/plugin-fs": "^2.4.4", - "@tauri-apps/plugin-log": "^2.7.1", - "@tauri-apps/plugin-opener": "^2.5.2", + "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", + "@tauri-apps/plugin-log": "^2.8.0", + "@tauri-apps/plugin-opener": "^2.5.4", "@tauri-apps/plugin-os": "^2.3.2", - "@tauri-apps/plugin-shell": "^2.3.3", + "@tauri-apps/plugin-shell": "^2.3.5", "buffer": "^6.0.3", "classnames": "^2.5.1", "cm6-graphql": "^0.2.1", @@ -52,6 +52,7 @@ "hexy": "^0.3.5", "history": "^5.3.0", "jotai": "^2.18.0", + "jotai-family": "^1.0.1", "js-md5": "^0.8.3", "lucide-react": "^0.525.0", "mime": "^4.0.4", diff --git a/apps/yaak-client/vite.config.ts b/apps/yaak-client/vite.config.ts index 1e72e4c6..00756360 100644 --- a/apps/yaak-client/vite.config.ts +++ b/apps/yaak-client/vite.config.ts @@ -3,7 +3,7 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { createRequire } from "node:module"; import path from "node:path"; -import { defineConfig, normalizePath } from "vite"; +import { defineConfig, normalizePath } from "vite-plus"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svgr from "vite-plugin-svgr"; import topLevelAwait from "vite-plugin-top-level-await"; @@ -42,12 +42,15 @@ export default defineConfig(async () => { sourcemap: true, outDir: "../../dist/apps/yaak-client", emptyOutDir: true, - rollupOptions: { + rolldownOptions: { output: { // Make chunk names readable chunkFileNames: "assets/chunk-[name]-[hash].js", entryFileNames: "assets/entry-[name]-[hash].js", assetFileNames: "assets/asset-[name]-[hash][extname]", + // Vite-Plus/Rolldown 0.1.20 can emit a stale style-mod export when + // top-level var rewriting combines with OXC minification. + topLevelVar: false, }, }, }, diff --git a/apps/yaak-proxy/components/Sidebar.tsx b/apps/yaak-proxy/components/Sidebar.tsx index f001dc6a..74c19cfc 100644 --- a/apps/yaak-proxy/components/Sidebar.tsx +++ b/apps/yaak-proxy/components/Sidebar.tsx @@ -2,7 +2,7 @@ import type { HttpExchange } from "@yaakapp-internal/proxy-lib"; import type { TreeNode } from "@yaakapp-internal/ui"; import { selectedIdsFamily, Tree } from "@yaakapp-internal/ui"; import { atom, useAtomValue } from "jotai"; -import { atomFamily } from "jotai/utils"; +import { atomFamily } from "jotai-family"; import { useCallback } from "react"; import { httpExchangesAtom } from "../lib/store"; diff --git a/apps/yaak-proxy/package.json b/apps/yaak-proxy/package.json index 4b2f6323..0386c049 100644 --- a/apps/yaak-proxy/package.json +++ b/apps/yaak-proxy/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.90.5", - "@tauri-apps/api": "^2.9.1", + "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-os": "^2.3.2", "@yaakapp-internal/model-store": "^1.0.0", "@yaakapp-internal/proxy-lib": "^1.0.0", @@ -18,6 +18,7 @@ "@yaakapp-internal/ui": "^1.0.0", "classnames": "^2.5.1", "jotai": "^2.18.0", + "jotai-family": "^1.0.1", "motion": "^12.4.7", "react": "^19.2.0", "react-dom": "^19.2.0" diff --git a/crates-proxy/yaak-proxy-lib/src/actions.rs b/crates-proxy/yaak-proxy-lib/src/actions.rs index 278e97a6..cf2084eb 100644 --- a/crates-proxy/yaak-proxy-lib/src/actions.rs +++ b/crates-proxy/yaak-proxy-lib/src/actions.rs @@ -25,11 +25,7 @@ pub struct ActionMetadata { } fn default_hotkey(mac: &str, other: &str) -> Option { - if cfg!(target_os = "macos") { - Some(mac.into()) - } else { - Some(other.into()) - } + if cfg!(target_os = "macos") { Some(mac.into()) } else { Some(other.into()) } } /// All global actions with their metadata, used by `list_actions` RPC. diff --git a/crates-proxy/yaak-proxy-lib/src/db.rs b/crates-proxy/yaak-proxy-lib/src/db.rs index d0332738..e5bba29d 100644 --- a/crates-proxy/yaak-proxy-lib/src/db.rs +++ b/crates-proxy/yaak-proxy-lib/src/db.rs @@ -14,10 +14,8 @@ pub struct ProxyQueryManager { impl ProxyQueryManager { pub fn new(db_path: &Path) -> Self { let manager = SqliteConnectionManager::file(db_path); - let pool = Pool::builder() - .max_size(5) - .build(manager) - .expect("Failed to create proxy DB pool"); + let pool = + Pool::builder().max_size(5).build(manager).expect("Failed to create proxy DB pool"); run_migrations(&pool, &MIGRATIONS).expect("Failed to run proxy DB migrations"); Self { pool } } diff --git a/crates-proxy/yaak-proxy-lib/src/lib.rs b/crates-proxy/yaak-proxy-lib/src/lib.rs index 36ddaf25..af72a6b6 100644 --- a/crates-proxy/yaak-proxy-lib/src/lib.rs +++ b/crates-proxy/yaak-proxy-lib/src/lib.rs @@ -2,18 +2,18 @@ pub mod actions; pub mod db; pub mod models; +use crate::actions::{ActionInvocation, ActionMetadata, GlobalAction}; +use crate::db::ProxyQueryManager; +use crate::models::{HttpExchange, ModelPayload, ProxyHeader}; +use log::warn; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::Path; use std::sync::Mutex; -use log::warn; -use serde::{Deserialize, Serialize}; use ts_rs::TS; use yaak_database::{ModelChangeEvent, UpdateSource}; use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState}; use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc}; -use crate::actions::{ActionInvocation, ActionMetadata, GlobalAction}; -use crate::db::ProxyQueryManager; -use crate::models::{HttpExchange, ModelPayload, ProxyHeader}; // -- Context -- @@ -25,11 +25,7 @@ pub struct ProxyCtx { impl ProxyCtx { pub fn new(db_path: &Path, events: RpcEventEmitter) -> Self { - Self { - handle: Mutex::new(None), - db: ProxyQueryManager::new(db_path), - events, - } + Self { handle: Mutex::new(None), db: ProxyQueryManager::new(db_path), events } } } @@ -88,17 +84,15 @@ fn execute_action(ctx: &ProxyCtx, invocation: ActionInvocation) -> Result match action { GlobalAction::ProxyStart => { - let mut handle = ctx - .handle - .lock() - .map_err(|_| RpcError { message: "lock poisoned".into() })?; + let mut handle = + ctx.handle.lock().map_err(|_| RpcError { message: "lock poisoned".into() })?; if handle.is_some() { return Ok(true); // already running } - let mut proxy_handle = yaak_proxy::start_proxy(9090) - .map_err(|e| RpcError { message: e })?; + let mut proxy_handle = + yaak_proxy::start_proxy(9090).map_err(|e| RpcError { message: e })?; if let Some(event_rx) = proxy_handle.take_event_rx() { let db = ctx.db.clone(); @@ -107,49 +101,43 @@ fn execute_action(ctx: &ProxyCtx, invocation: ActionInvocation) -> Result { - let mut handle = ctx - .handle - .lock() - .map_err(|_| RpcError { message: "lock poisoned".into() })?; + let mut handle = + ctx.handle.lock().map_err(|_| RpcError { message: "lock poisoned".into() })?; handle.take(); - ctx.events.emit("proxy_state_changed", &ProxyStatePayload { - state: ProxyState::Stopped, - }); + ctx.events + .emit("proxy_state_changed", &ProxyStatePayload { state: ProxyState::Stopped }); Ok(true) } }, } } -fn get_proxy_state(ctx: &ProxyCtx, _req: GetProxyStateRequest) -> Result { - let handle = ctx - .handle - .lock() - .map_err(|_| RpcError { message: "lock poisoned".into() })?; - let state = if handle.is_some() { - ProxyState::Running - } else { - ProxyState::Stopped - }; +fn get_proxy_state( + ctx: &ProxyCtx, + _req: GetProxyStateRequest, +) -> Result { + let handle = ctx.handle.lock().map_err(|_| RpcError { message: "lock poisoned".into() })?; + let state = if handle.is_some() { ProxyState::Running } else { ProxyState::Stopped }; Ok(GetProxyStateResponse { state }) } -fn list_actions(_ctx: &ProxyCtx, _req: ListActionsRequest) -> Result { - Ok(ListActionsResponse { - actions: crate::actions::all_global_actions(), - }) +fn list_actions( + _ctx: &ProxyCtx, + _req: ListActionsRequest, +) -> Result { + Ok(ListActionsResponse { actions: crate::actions::all_global_actions() }) } fn list_models(ctx: &ProxyCtx, _req: ListModelsRequest) -> Result { ctx.db.with_conn(|db| { Ok(ListModelsResponse { - http_exchanges: db.find_all::() + http_exchanges: db + .find_all::() .map_err(|e| RpcError { message: e.to_string() })?, }) }) @@ -157,28 +145,35 @@ fn list_models(ctx: &ProxyCtx, _req: ListModelsRequest) -> Result, db: ProxyQueryManager, events: RpcEventEmitter) { +fn run_event_loop( + rx: std::sync::mpsc::Receiver, + db: ProxyQueryManager, + events: RpcEventEmitter, +) { let mut in_flight: HashMap = HashMap::new(); while let Ok(event) = rx.recv() { match event { ProxyEvent::RequestStart { id, method, url, http_version } => { - in_flight.insert(id, CapturedRequest { + in_flight.insert( id, - method, - url, - http_version, - status: None, - elapsed_ms: None, - remote_http_version: None, - request_headers: vec![], - request_body: None, - response_headers: vec![], - response_body: None, - response_body_size: 0, - state: RequestState::Sending, - error: None, - }); + CapturedRequest { + id, + method, + url, + http_version, + status: None, + elapsed_ms: None, + remote_http_version: None, + request_headers: vec![], + request_body: None, + response_headers: vec![], + response_body: None, + response_body_size: 0, + state: RequestState::Sending, + error: None, + }, + ); } ProxyEvent::RequestHeader { id, name, value } => { if let Some(r) = in_flight.get_mut(&id) { @@ -230,28 +225,30 @@ fn write_entry(db: &ProxyQueryManager, events: &RpcEventEmitter, r: &CapturedReq let entry = HttpExchange { url: r.url.clone(), method: r.method.clone(), - req_headers: r.request_headers.iter() + req_headers: r + .request_headers + .iter() .map(|(n, v)| ProxyHeader { name: n.clone(), value: v.clone() }) .collect(), req_body: r.request_body.clone(), res_status: r.status.map(|s| s as i32), - res_headers: r.response_headers.iter() + res_headers: r + .response_headers + .iter() .map(|(n, v)| ProxyHeader { name: n.clone(), value: v.clone() }) .collect(), res_body: r.response_body.clone(), error: r.error.clone(), ..Default::default() }; - db.with_conn(|ctx| { - match ctx.upsert(&entry, &UpdateSource::Background) { - Ok((saved, created)) => { - events.emit("model_write", &ModelPayload { - model: saved, - change: ModelChangeEvent::Upsert { created }, - }); - } - Err(e) => warn!("Failed to write proxy entry: {e}"), + db.with_conn(|ctx| match ctx.upsert(&entry, &UpdateSource::Background) { + Ok((saved, created)) => { + events.emit( + "model_write", + &ModelPayload { model: saved, change: ModelChangeEvent::Upsert { created } }, + ); } + Err(e) => warn!("Failed to write proxy entry: {e}"), }); } diff --git a/crates-proxy/yaak-proxy-lib/src/models.rs b/crates-proxy/yaak-proxy-lib/src/models.rs index 3c1d5624..0b6249ae 100644 --- a/crates-proxy/yaak-proxy-lib/src/models.rs +++ b/crates-proxy/yaak-proxy-lib/src/models.rs @@ -3,7 +3,10 @@ use rusqlite::Row; use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_def}; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use yaak_database::{ModelChangeEvent, Result as DbResult, UpdateSource, UpsertModelInfo, generate_prefixed_id, upsert_date}; +use yaak_database::{ + ModelChangeEvent, Result as DbResult, UpdateSource, UpsertModelInfo, generate_prefixed_id, + upsert_date, +}; #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] diff --git a/crates-tauri/yaak-app-client/Cargo.toml b/crates-tauri/yaak-app-client/Cargo.toml index be185b7d..177fff84 100644 --- a/crates-tauri/yaak-app-client/Cargo.toml +++ b/crates-tauri/yaak-app-client/Cargo.toml @@ -17,7 +17,7 @@ updater = [] license = ["yaak-license"] [build-dependencies] -tauri-build = { version = "2.5.3", features = [] } +tauri-build = { version = "2.6.1", features = [] } [target.'cfg(target_os = "linux")'.dependencies] openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work @@ -30,6 +30,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client http = { version = "1.2.0", default-features = false } log = { workspace = true } md5 = "0.8.0" +notify = "8.0.0" pretty_graphql = "0.2" r2d2 = "0.8.10" r2d2_sqlite = "0.25.0" @@ -49,15 +50,15 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["raw_value"] } tauri = { workspace = true, features = ["devtools", "protocol-asset"] } tauri-plugin-clipboard-manager = "2.3.2" -tauri-plugin-deep-link = "2.4.5" +tauri-plugin-deep-link = "2.4.9" tauri-plugin-dialog = { workspace = true } -tauri-plugin-fs = "2.4.4" -tauri-plugin-log = { version = "2.7.1", features = ["colored"] } -tauri-plugin-opener = "2.5.2" +tauri-plugin-fs = "2.5.1" +tauri-plugin-log = { version = "2.8.0", features = ["colored"] } +tauri-plugin-opener = "2.5.4" tauri-plugin-os = "2.3.2" tauri-plugin-shell = { workspace = true } -tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] } -tauri-plugin-updater = "2.9.0" +tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] } +tauri-plugin-updater = "2.10.1" tauri-plugin-window-state = "2.4.1" thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } diff --git a/crates-tauri/yaak-app-client/bindings/index.ts b/crates-tauri/yaak-app-client/bindings/index.ts index 8f19e775..28d15eac 100644 --- a/crates-tauri/yaak-app-client/bindings/index.ts +++ b/crates-tauri/yaak-app-client/bindings/index.ts @@ -12,6 +12,8 @@ export type UpdateResponseAction = "install" | "skip"; export type WatchResult = { unlistenEvent: string, }; +export type GitWatchResult = { unlistenEvent: string, }; + export type YaakNotification = { timestamp: string, timeout: number | null, id: string, title: string | null, message: string, color: string | null, action: YaakNotificationAction | null, }; export type YaakNotificationAction = { label: string, url: string, }; diff --git a/crates-tauri/yaak-app-client/src/git_ext.rs b/crates-tauri/yaak-app-client/src/git_ext.rs index c4ae297a..004d65e0 100644 --- a/crates-tauri/yaak-app-client/src/git_ext.rs +++ b/crates-tauri/yaak-app-client/src/git_ext.rs @@ -3,14 +3,18 @@ //! This module provides the Tauri commands for git functionality. use crate::error::Result; +use crate::git_watcher::{GitWatchResult, watch_git_worktree_status}; use std::path::{Path, PathBuf}; -use tauri::command; +use tauri::ipc::Channel; +use tauri::{AppHandle, Runtime, command}; use yaak_git::{ - BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, - PushResult, git_add, git_add_credential, git_add_remote, git_checkout_branch, git_clone, - git_commit, git_create_branch, git_delete_branch, git_delete_remote_branch, git_fetch_all, - git_init, git_log, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, git_push, - git_remotes, git_rename_branch, git_reset_changes, git_rm_remote, git_status, git_unstage, + BranchDeleteResult, CloneResult, GitBranchInfo, GitCommit, GitFileDiff, GitRemote, + GitStatusSummary, GitWorktreeStatus, PullResult, PushResult, git_add, git_add_credential, + git_add_remote, git_branch_info, git_checkout_branch, git_clone, git_commit, git_create_branch, + git_delete_branch, git_delete_remote_branch, git_fetch_all, git_file_diff_for_commit, git_init, + git_log, git_log_for_file, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, + git_push, git_remotes, git_rename_branch, git_reset_changes, git_restore, + git_restore_file_from_commit, git_rm_remote, git_status, git_unstage, git_worktree_status, }; // NOTE: All of these commands are async to prevent blocking work from locking up the UI @@ -54,11 +58,44 @@ pub async fn cmd_git_status(dir: &Path) -> Result { Ok(git_status(dir)?) } +#[command] +pub async fn cmd_git_branch_info(dir: &Path) -> Result { + Ok(git_branch_info(dir)?) +} + +#[command] +pub async fn cmd_git_worktree_status(dir: &Path) -> Result { + Ok(git_worktree_status(dir)?) +} + +#[command] +pub async fn cmd_git_watch_worktree_status( + app_handle: AppHandle, + dir: &Path, + channel: Channel, +) -> Result { + watch_git_worktree_status(app_handle, dir, channel).await +} + #[command] pub async fn cmd_git_log(dir: &Path) -> Result> { Ok(git_log(dir)?) } +#[command] +pub async fn cmd_git_log_for_file(dir: &Path, rela_path: PathBuf) -> Result> { + Ok(git_log_for_file(dir, &rela_path)?) +} + +#[command] +pub async fn cmd_git_file_diff_for_commit( + dir: &Path, + commit_oid: &str, + rela_path: PathBuf, +) -> Result { + Ok(git_file_diff_for_commit(dir, commit_oid, &rela_path)?) +} + #[command] pub async fn cmd_git_initialize(dir: &Path) -> Result<()> { Ok(git_init(dir)?) @@ -124,6 +161,23 @@ pub async fn cmd_git_reset_changes(dir: &Path) -> Result<()> { Ok(git_reset_changes(dir).await?) } +#[command] +pub async fn cmd_git_restore_files(dir: &Path, rela_paths: Vec) -> Result<()> { + for path in rela_paths { + git_restore(dir, &path)?; + } + Ok(()) +} + +#[command] +pub async fn cmd_git_restore_file_from_commit( + dir: &Path, + commit_oid: &str, + rela_path: PathBuf, +) -> Result<()> { + Ok(git_restore_file_from_commit(dir, commit_oid, &rela_path)?) +} + #[command] pub async fn cmd_git_add_credential( remote_url: &str, diff --git a/crates-tauri/yaak-app-client/src/git_watcher.rs b/crates-tauri/yaak-app-client/src/git_watcher.rs new file mode 100644 index 00000000..b82faba8 --- /dev/null +++ b/crates-tauri/yaak-app-client/src/git_watcher.rs @@ -0,0 +1,172 @@ +use crate::error::{Error, Result}; +use chrono::Utc; +use log::{debug, error, warn}; +use notify::Watcher; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::sync::mpsc; +use std::time::Duration; +use tauri::ipc::Channel; +use tauri::{AppHandle, Listener, Runtime}; +use tokio::select; +use tokio::sync::watch; +use tokio::time::sleep; +use ts_rs::TS; +use yaak_git::{GitWorktreeStatus, git_path_is_ignored, git_repository_paths, git_worktree_status}; + +const GIT_STATUS_COALESCE_WINDOW: Duration = Duration::from_millis(250); + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "index.ts")] +pub(crate) struct GitWatchResult { + unlisten_event: String, +} + +pub(crate) async fn watch_git_worktree_status( + app_handle: AppHandle, + dir: &Path, + channel: Channel, +) -> Result { + let paths = git_repository_paths(dir)?; + let repo_dir = dir.to_path_buf(); + let workdir = paths.workdir; + let gitdir = paths.gitdir; + + let (tx, rx) = mpsc::channel::>(); + let mut watcher = notify::recommended_watcher(tx) + .map_err(|e| Error::GenericError(format!("Failed to watch Git repository: {e}")))?; + + watcher + .watch(&workdir, notify::RecursiveMode::Recursive) + .map_err(|e| Error::GenericError(format!("Failed to watch Git worktree: {e}")))?; + if gitdir != workdir { + watcher + .watch(&gitdir, notify::RecursiveMode::Recursive) + .map_err(|e| Error::GenericError(format!("Failed to watch Git metadata: {e}")))?; + } + + let (async_tx, mut async_rx) = tokio::sync::mpsc::channel::>(100); + std::thread::spawn(move || { + for res in rx { + if async_tx.blocking_send(res).is_err() { + break; + } + } + }); + + let (cancel_tx, cancel_rx) = watch::channel(()); + let mut cancel_rx = cancel_rx; + send_worktree_status(&repo_dir, &channel); + + tauri::async_runtime::spawn(async move { + let _watcher = watcher; + loop { + select! { + Some(event_res) = async_rx.recv() => { + handle_git_watch_event( + event_res, + &mut async_rx, + &repo_dir, + &workdir, + &gitdir, + &channel, + ).await; + } + _ = cancel_rx.changed() => { + break; + } + } + } + }); + + let app_handle_inner = app_handle.clone(); + let unlisten_event = format!("git-watch-unlisten-{}", Utc::now().timestamp_millis()); + app_handle.listen_any(unlisten_event.clone(), move |event| { + app_handle_inner.unlisten(event.id()); + if let Err(e) = cancel_tx.send(()) { + warn!("Failed to send git watch cancel signal {e:?}"); + } + }); + + Ok(GitWatchResult { unlisten_event }) +} + +async fn handle_git_watch_event( + event_res: notify::Result, + async_rx: &mut tokio::sync::mpsc::Receiver>, + repo_dir: &Path, + workdir: &Path, + gitdir: &Path, + channel: &Channel, +) { + if !is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir) { + return; + } + + send_worktree_status(repo_dir, channel); + + let settle_window = sleep(GIT_STATUS_COALESCE_WINDOW); + tokio::pin!(settle_window); + loop { + select! { + Some(event_res) = async_rx.recv() => { + let _ = is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir); + } + _ = &mut settle_window => { + break; + } + } + } + + send_worktree_status(repo_dir, channel); +} + +fn is_relevant_git_watch_event( + event_res: notify::Result, + repo_dir: &Path, + workdir: &Path, + gitdir: &Path, +) -> bool { + let event = match event_res { + Ok(event) => event, + Err(e) => { + error!("Git watch error: {:?}", e); + return false; + } + }; + + for path in event.paths { + if path.strip_prefix(gitdir).is_ok() { + return true; + } + + let Ok(rela_path) = path.strip_prefix(workdir) else { + continue; + }; + + match git_path_is_ignored(repo_dir, rela_path) { + Ok(true) => {} + Ok(false) => return true, + Err(e) => { + debug!("Failed to check Git ignore status for {:?}: {e}", rela_path); + return true; + } + } + } + + false +} + +fn send_worktree_status(repo_dir: &Path, channel: &Channel) { + match git_worktree_status(repo_dir) { + Ok(status) => { + if let Err(e) = channel.send(status) { + warn!("Failed to send git worktree status: {:?}", e); + } + } + Err(e) => { + warn!("Failed to get git worktree status: {e}"); + } + } +} diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index aea200c5..817d05d0 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -67,6 +67,7 @@ mod commands; mod encoding; mod error; mod git_ext; +mod git_watcher; mod grpc; mod history; mod http_request; @@ -121,9 +122,7 @@ fn setup_window_menu(win: &WebviewWindow) -> Result<()> { } // Commands for development - "dev.reset_size" => webview_window - .set_size(LogicalSize::new(1100.0, 600.0)) - .unwrap(), + "dev.reset_size" => webview_window.set_size(LogicalSize::new(1100.0, 600.0)).unwrap(), "dev.reset_size_16x9" => { let width = webview_window.outer_size().unwrap().width; let height = width * 9 / 16; @@ -1506,7 +1505,6 @@ async fn cmd_reload_plugins( Ok(errors) } - #[tauri::command] async fn cmd_plugin_info( id: &str, @@ -1579,7 +1577,14 @@ async fn cmd_new_child_window( inner_size: (f64, f64), ) -> YaakResult<()> { let use_native_titlebar = parent_window.app_handle().db().get_settings().use_native_titlebar; - let win = yaak_window::window::create_child_window(&parent_window, url, label, title, inner_size, use_native_titlebar)?; + let win = yaak_window::window::create_child_window( + &parent_window, + url, + label, + title, + inner_size, + use_native_titlebar, + )?; setup_window_menu(&win)?; Ok(()) } @@ -1831,8 +1836,13 @@ pub fn run() { git_ext::cmd_git_delete_remote_branch, git_ext::cmd_git_merge_branch, git_ext::cmd_git_rename_branch, + git_ext::cmd_git_branch_info, git_ext::cmd_git_status, + git_ext::cmd_git_worktree_status, + git_ext::cmd_git_watch_worktree_status, git_ext::cmd_git_log, + git_ext::cmd_git_log_for_file, + git_ext::cmd_git_file_diff_for_commit, git_ext::cmd_git_initialize, git_ext::cmd_git_clone, git_ext::cmd_git_commit, @@ -1844,6 +1854,8 @@ pub fn run() { git_ext::cmd_git_add, git_ext::cmd_git_unstage, git_ext::cmd_git_reset_changes, + git_ext::cmd_git_restore_files, + git_ext::cmd_git_restore_file_from_commit, git_ext::cmd_git_add_credential, git_ext::cmd_git_remotes, git_ext::cmd_git_add_remote, @@ -1870,7 +1882,11 @@ pub fn run() { match event { RunEvent::Ready => { let use_native_titlebar = app_handle.db().get_settings().use_native_titlebar; - if let Ok(win) = yaak_window::window::create_main_window(app_handle, "/", use_native_titlebar) { + if let Ok(win) = yaak_window::window::create_main_window( + app_handle, + "/", + use_native_titlebar, + ) { let _ = setup_window_menu(&win); } let h = app_handle.clone(); diff --git a/crates-tauri/yaak-app-client/src/plugin_events.rs b/crates-tauri/yaak-app-client/src/plugin_events.rs index 6afe6e4a..4b14dd90 100644 --- a/crates-tauri/yaak-app-client/src/plugin_events.rs +++ b/crates-tauri/yaak-app-client/src/plugin_events.rs @@ -3,7 +3,6 @@ use crate::http_request::send_http_request_with_context; use crate::models_ext::BlobManagerExt; use crate::models_ext::QueryManagerExt; use crate::render::{render_grpc_request, render_http_request, render_json_value}; -use yaak_window::window::{CreateWindowConfig, create_window}; use crate::{ call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_plugin_context, workspace_from_window, @@ -36,6 +35,7 @@ use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_tauri_utils::window::WorkspaceWindowTrait; use yaak_templates::{RenderErrorBehavior, RenderOptions}; +use yaak_window::window::{CreateWindowConfig, create_window}; pub(crate) async fn handle_plugin_event( app_handle: &AppHandle, diff --git a/crates-tauri/yaak-app-client/src/updates.rs b/crates-tauri/yaak-app-client/src/updates.rs index 6fd7fe74..d55465ba 100644 --- a/crates-tauri/yaak-app-client/src/updates.rs +++ b/crates-tauri/yaak-app-client/src/updates.rs @@ -234,7 +234,7 @@ async fn start_integrated_update( window: &WebviewWindow, update: &Update, ) -> Result { - let download_path = ensure_download_path(window, update)?; + let download_path = ensure_download_dir(window)?.join(download_file_name(update)); debug!("Download path: {}", download_path.display()); let downloaded = download_path.exists(); let ack_wait = Duration::from_secs(3); @@ -345,7 +345,7 @@ pub async fn download_update_idempotent( window: &WebviewWindow, update: &Update, ) -> Result { - let dl_path = ensure_download_path(window, update)?; + let dl_path = ensure_download_dir(window)?.join(download_file_name(update)); if dl_path.exists() { info!("{} already downloaded to {}", update.version, dl_path.display()); @@ -385,21 +385,36 @@ pub async fn install_update_maybe_download( let dl_path = download_update_idempotent(window, update).await?; let update_bytes = std::fs::read(&dl_path)?; update.install(update_bytes.as_slice())?; + delete_download_dir(window); Ok(()) } -pub fn ensure_download_path( - window: &WebviewWindow, - update: &Update, -) -> Result { - // Ensure dir exists - let base_dir = window.path().app_cache_dir()?.join("updates"); - std::fs::create_dir_all(&base_dir)?; - - // Generate name based on signature - let sig_digest = md5::compute(&update.signature); - let name = format!("yaak-{}-{:x}", update.version, sig_digest); - let dl_path = base_dir.join(name); - - Ok(dl_path) +pub fn download_dir(window: &WebviewWindow) -> Result { + Ok(window.path().app_cache_dir()?.join("updates")) +} + +pub fn ensure_download_dir(window: &WebviewWindow) -> Result { + let base_dir = download_dir(window)?; + std::fs::create_dir_all(&base_dir)?; + Ok(base_dir) +} + +pub fn download_file_name(update: &Update) -> String { + let sig_digest = md5::compute(&update.signature); + format!("yaak-{}-{:x}", update.version, sig_digest) +} + +pub fn delete_download_dir(window: &WebviewWindow) { + let base_dir = match download_dir(window) { + Ok(dir) => dir, + Err(e) => { + warn!("Failed to locate update downloads dir: {}", e); + return; + } + }; + match std::fs::remove_dir_all(&base_dir) { + Ok(()) => info!("Removed update downloads dir {}", base_dir.display()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => warn!("Failed to remove update downloads dir {}: {}", base_dir.display(), e), + } } diff --git a/crates-tauri/yaak-app-proxy/Cargo.toml b/crates-tauri/yaak-app-proxy/Cargo.toml index 0adda828..74581bbd 100644 --- a/crates-tauri/yaak-app-proxy/Cargo.toml +++ b/crates-tauri/yaak-app-proxy/Cargo.toml @@ -10,7 +10,7 @@ name = "tauri_app_proxy_lib" crate-type = ["staticlib", "cdylib", "lib"] [build-dependencies] -tauri-build = { version = "2.5.3", features = [] } +tauri-build = { version = "2.6.1", features = [] } [dependencies] log = { workspace = true } diff --git a/crates-tauri/yaak-app-proxy/src/lib.rs b/crates-tauri/yaak-app-proxy/src/lib.rs index 6cd23350..b66a78a4 100644 --- a/crates-tauri/yaak-app-proxy/src/lib.rs +++ b/crates-tauri/yaak-app-proxy/src/lib.rs @@ -1,6 +1,6 @@ use log::{error, info, warn}; -use tauri::{Emitter, Manager, RunEvent, State, WebviewWindow}; use tauri::Runtime; +use tauri::{Emitter, Manager, RunEvent, State, WebviewWindow}; use yaak_proxy_lib::ProxyCtx; use yaak_rpc::{RpcEventEmitter, RpcRouter}; use yaak_window::window::CreateWindowConfig; diff --git a/crates-tauri/yaak-mac-window/src/mac.rs b/crates-tauri/yaak-mac-window/src/mac.rs index c5b5cdba..e45d0bd4 100644 --- a/crates-tauri/yaak-mac-window/src/mac.rs +++ b/crates-tauri/yaak-mac-window/src/mac.rs @@ -109,19 +109,16 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, // we've modified it. This avoids the height growing on repeated calls. use std::sync::OnceLock; static DEFAULT_TITLEBAR_HEIGHT: OnceLock = OnceLock::new(); - let default_height = - *DEFAULT_TITLEBAR_HEIGHT.get_or_init(|| NSView::frame(title_bar_container_view).size.height); + let default_height = *DEFAULT_TITLEBAR_HEIGHT + .get_or_init(|| NSView::frame(title_bar_container_view).size.height); // On pre-Tahoe, button_height + y is larger than the default title bar // height, so the resize works as before. On Tahoe (26+), the default is // already 32px and button_height + y = 32, so nothing changes. In that // case, add TITLEBAR_EXTRA_HEIGHT extra pixels to push the buttons down. let desired = button_height + y; - let title_bar_frame_height = if desired > default_height { - desired - } else { - default_height + TITLEBAR_EXTRA_HEIGHT - }; + let title_bar_frame_height = + if desired > default_height { desired } else { default_height + TITLEBAR_EXTRA_HEIGHT }; let mut title_bar_rect = NSView::frame(title_bar_container_view); title_bar_rect.size.height = title_bar_frame_height; diff --git a/crates/common/yaak-database/src/db_context.rs b/crates/common/yaak-database/src/db_context.rs index 83e56711..8303e89d 100644 --- a/crates/common/yaak-database/src/db_context.rs +++ b/crates/common/yaak-database/src/db_context.rs @@ -65,8 +65,7 @@ impl<'a> DbContext<'a> { .cond_where(Expr::col(col).eq(value)) .build_rusqlite(SqliteQueryBuilder); let mut stmt = self.conn.prepare(sql.as_str()).expect("Failed to prepare query"); - stmt.query_row(&*params.as_params(), M::from_row) - .ok() + stmt.query_row(&*params.as_params(), M::from_row).ok() } pub fn find_all(&self) -> Result> @@ -126,9 +125,8 @@ impl<'a> DbContext<'a> { let other_values = model.clone().insert_values(source)?; let mut column_vec = vec![id_iden.clone()]; - let mut value_vec = vec![ - if id_val.is_empty() { M::generate_id().into() } else { id_val.into() }, - ]; + let mut value_vec = + vec![if id_val.is_empty() { M::generate_id().into() } else { id_val.into() }]; for (col, val) in other_values { value_vec.push(val.into()); diff --git a/crates/common/yaak-database/src/migrate.rs b/crates/common/yaak-database/src/migrate.rs index c81b0c21..30c53dcd 100644 --- a/crates/common/yaak-database/src/migrate.rs +++ b/crates/common/yaak-database/src/migrate.rs @@ -55,8 +55,7 @@ pub fn run_migrations(pool: &Pool, dir: &Dir<'_>) -> Re continue; } - let sql = - entry.as_file().unwrap().contents_utf8().expect("Failed to read migration file"); + let sql = entry.as_file().unwrap().contents_utf8().expect("Failed to read migration file"); info!("Applying migration: {}", filename); let conn = pool.get()?; diff --git a/crates/common/yaak-database/src/util.rs b/crates/common/yaak-database/src/util.rs index a9c84468..64af7c80 100644 --- a/crates/common/yaak-database/src/util.rs +++ b/crates/common/yaak-database/src/util.rs @@ -10,10 +10,10 @@ pub fn generate_id() -> String { pub fn generate_id_of_length(n: usize) -> String { let alphabet: [char; 57] = [ - '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', - 'j', 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', - 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', - 'U', 'V', 'W', 'X', 'Y', 'Z', + '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', + 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', ]; nanoid!(n, &alphabet) diff --git a/crates/common/yaak-rpc/src/lib.rs b/crates/common/yaak-rpc/src/lib.rs index 8895c610..80a462bf 100644 --- a/crates/common/yaak-rpc/src/lib.rs +++ b/crates/common/yaak-rpc/src/lib.rs @@ -3,7 +3,8 @@ use std::collections::HashMap; use std::sync::mpsc; /// Type-erased handler function: takes context + JSON payload, returns JSON or error. -type HandlerFn = Box Result + Send + Sync>; +type HandlerFn = + Box Result + Send + Sync>; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RpcError { @@ -57,9 +58,7 @@ pub struct RpcRouter { impl RpcRouter { pub fn new() -> Self { - Self { - handlers: HashMap::new(), - } + Self { handlers: HashMap::new() } } /// Register a handler for a command name. @@ -77,23 +76,15 @@ impl RpcRouter { ) -> Result { match self.handlers.get(cmd) { Some(handler) => handler(ctx, payload), - None => Err(RpcError { - message: format!("unknown command: {cmd}"), - }), + None => Err(RpcError { message: format!("unknown command: {cmd}") }), } } /// Handle a full `RpcRequest`, returning an `RpcResponse`. pub fn handle(&self, req: RpcRequest, ctx: &Ctx) -> RpcResponse { match self.dispatch(&req.cmd, req.payload, ctx) { - Ok(payload) => RpcResponse::Success { - id: req.id, - payload, - }, - Err(e) => RpcResponse::Error { - id: req.id, - error: e.message, - }, + Ok(payload) => RpcResponse::Success { id: req.id, payload }, + Err(e) => RpcResponse::Error { id: req.id, error: e.message }, } } diff --git a/crates/yaak-git/bindings/gen_git.ts b/crates/yaak-git/bindings/gen_git.ts index 1ec69d86..3222a770 100644 --- a/crates/yaak-git/bindings/gen_git.ts +++ b/crates/yaak-git/bindings/gen_git.ts @@ -7,7 +7,11 @@ export type CloneResult = { "type": "success" } | { "type": "cancelled" } | { "t export type GitAuthor = { name: string | null, email: string | null, }; -export type GitCommit = { author: GitAuthor, when: string, message: string | null, }; +export type GitBranchInfo = { path: string, headRef: string | null, headRefShorthand: string | null, origins: Array, localBranches: Array, remoteBranches: Array, ahead: number, behind: number, }; + +export type GitCommit = { oid: string, author: GitAuthor, when: string, message: string | null, }; + +export type GitFileDiff = { original: string, modified: string, }; export type GitRemote = { name: string, url: string | null, }; @@ -17,6 +21,10 @@ export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: bool export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array, origins: Array, localBranches: Array, remoteBranches: Array, ahead: number, behind: number, }; +export type GitWorktreeStatus = { entries: Array, }; + +export type GitWorktreeStatusEntry = { relaPath: string, modelId: string | null, status: GitStatus, staged: boolean, }; + export type PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, } | { "type": "diverged", remote: string, branch: string, } | { "type": "uncommitted_changes" }; export type PushResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, }; diff --git a/crates/yaak-git/index.ts b/crates/yaak-git/index.ts index 418def3e..ea7ef76a 100644 --- a/crates/yaak-git/index.ts +++ b/crates/yaak-git/index.ts @@ -1,14 +1,18 @@ import { useQuery } from "@tanstack/react-query"; -import { invoke } from "@tauri-apps/api/core"; +import { Channel, invoke } from "@tauri-apps/api/core"; +import { emit } from "@tauri-apps/api/event"; import { createFastMutation } from "@yaakapp/yaak-client/hooks/useFastMutation"; import { queryClient } from "@yaakapp/yaak-client/lib/queryClient"; import { useMemo } from "react"; import { BranchDeleteResult, CloneResult, + GitBranchInfo, GitCommit, + GitFileDiff, GitRemote, GitStatusSummary, + GitWorktreeStatus, PullResult, PushResult, } from "./bindings/gen_git"; @@ -26,6 +30,10 @@ export type DivergedStrategy = "force_reset" | "merge" | "cancel"; export type UncommittedChangesStrategy = "reset" | "cancel"; +interface GitWatchResult { + unlistenEvent: string; +} + export interface GitCallbacks { addRemote: () => Promise; promptCredentials: ( @@ -38,13 +46,98 @@ export interface GitCallbacks { const onSuccess = () => queryClient.invalidateQueries({ queryKey: ["git"] }); -export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) { - const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]); - const fetchAll = useQuery({ +function gitWorktreeStatusQueryKey(dir?: string, refreshKey?: string) { + return refreshKey == null + ? (["git", "worktree_status", dir] as const) + : (["git", "worktree_status", dir, refreshKey] as const); +} + +export function invalidateGitWorktreeStatus(dir?: string) { + return queryClient.invalidateQueries({ queryKey: gitWorktreeStatusQueryKey(dir) }); +} + +export function useGitWorktreeStatus(dir: string, refreshKey?: string) { + return useQuery({ + queryKey: gitWorktreeStatusQueryKey(dir, refreshKey), + queryFn: () => invoke("cmd_git_worktree_status", { dir }), + placeholderData: (prev) => prev, + }); +} + +export function watchGitWorktreeStatus(dir: string, callback: (status: GitWorktreeStatus) => void) { + const channel = new Channel(); + channel.onmessage = callback; + const unlistenPromise = invoke("cmd_git_watch_worktree_status", { + dir, + channel, + }); + + void unlistenPromise + .then(({ unlistenEvent }) => { + addGitWatchKey(unlistenEvent); + }) + .catch(console.debug); + + return () => + unlistenPromise + .then(async ({ unlistenEvent }) => { + unlistenGitWatcher(unlistenEvent); + }) + .catch(console.error); +} + +function useGitFetchAll(dir: string, refreshKey?: string) { + return useQuery({ queryKey: ["git", "fetch_all", dir, refreshKey], queryFn: () => invoke("cmd_git_fetch_all", { dir }), refetchInterval: 10 * 60_000, }); +} + +function useGitBranchInfoQuery(dir: string, refreshKey?: string, fetchAllUpdatedAt?: number) { + return useQuery({ + refetchOnMount: true, + queryKey: ["git", "branch_info", dir, refreshKey, fetchAllUpdatedAt], + queryFn: () => invoke("cmd_git_branch_info", { dir }), + placeholderData: (prev) => prev, + }); +} + +export function useGitBranchInfo(dir: string, refreshKey?: string) { + const fetchAll = useGitFetchAll(dir, refreshKey); + return useGitBranchInfoQuery(dir, refreshKey, fetchAll.dataUpdatedAt); +} + +export function useGitLog(dir: string, refreshKey?: string, relaPath?: string) { + return useQuery({ + queryKey: ["git", "log", dir, refreshKey, relaPath], + queryFn: () => + relaPath == null + ? invoke("cmd_git_log", { dir }) + : invoke("cmd_git_log_for_file", { dir, relaPath }), + placeholderData: (prev) => prev, + }); +} + +export function useGitFileDiffForCommit( + dir: string, + relaPath: string, + commitOid: string | null | undefined, +) { + return useQuery({ + enabled: commitOid != null, + queryKey: ["git", "file_diff_for_commit", dir, relaPath, commitOid], + queryFn: () => { + if (commitOid == null) throw new Error("Missing commit oid"); + return invoke("cmd_git_file_diff_for_commit", { dir, relaPath, commitOid }); + }, + }); +} + +export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) { + const mutations = useGitMutations(dir, callbacks); + const fetchAll = useGitFetchAll(dir, refreshKey); + return [ { remotes: useQuery({ @@ -52,11 +145,7 @@ export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string queryFn: () => getRemotes(dir), placeholderData: (prev) => prev, }), - log: useQuery({ - queryKey: ["git", "log", dir, refreshKey], - queryFn: () => invoke("cmd_git_log", { dir }), - placeholderData: (prev) => prev, - }), + log: useGitLog(dir, refreshKey), status: useQuery({ refetchOnMount: true, queryKey: ["git", "status", dir, refreshKey, fetchAll.dataUpdatedAt], @@ -68,6 +157,10 @@ export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string ] as const; } +export function useGitMutations(dir: string, callbacks: GitCallbacks) { + return useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]); +} + export const gitMutations = (dir: string, callbacks: GitCallbacks) => { const push = async () => { const remotes = await getRemotes(dir); @@ -250,6 +343,20 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => { mutationFn: () => invoke("cmd_git_reset_changes", { dir }), onSuccess, }), + restore: createFastMutation({ + mutationKey: ["git", "restore", dir], + mutationFn: (args) => invoke("cmd_git_restore_files", { dir, ...args }), + onSuccess, + }), + restoreFileFromCommit: createFastMutation< + void, + string, + { commitOid: string; relaPath: string } + >({ + mutationKey: ["git", "restore-file-from-commit", dir], + mutationFn: (args) => invoke("cmd_git_restore_file_from_commit", { dir, ...args }), + onSuccess, + }), } as const; }; @@ -257,6 +364,35 @@ async function getRemotes(dir: string) { return invoke("cmd_git_remotes", { dir }); } +function unlistenGitWatcher(unlistenEvent: string) { + void emit(unlistenEvent).then(() => { + removeGitWatchKey(unlistenEvent); + }); +} + +function getGitWatchKeys() { + return sessionStorage.getItem("git-worktree-watchers")?.split(",").filter(Boolean) ?? []; +} + +function setGitWatchKeys(keys: string[]) { + sessionStorage.setItem("git-worktree-watchers", keys.join(",")); +} + +function addGitWatchKey(key: string) { + const keys = getGitWatchKeys(); + setGitWatchKeys([...keys, key]); +} + +function removeGitWatchKey(key: string) { + const keys = getGitWatchKeys(); + setGitWatchKeys(keys.filter((k) => k !== key)); +} + +const gitWatchKeys = getGitWatchKeys(); +if (gitWatchKeys.length > 0) { + gitWatchKeys.forEach(unlistenGitWatcher); +} + /** * Clone a git repository, prompting for credentials if needed. */ diff --git a/crates/yaak-git/src/lib.rs b/crates/yaak-git/src/lib.rs index 2a0b6406..9500fa69 100644 --- a/crates/yaak-git/src/lib.rs +++ b/crates/yaak-git/src/lib.rs @@ -14,6 +14,7 @@ mod push; mod remotes; mod repository; mod reset; +mod restore; mod status; mod unstage; mod util; @@ -29,10 +30,15 @@ pub use commit::git_commit; pub use credential::git_add_credential; pub use fetch::git_fetch_all; pub use init::git_init; -pub use log::{GitCommit, git_log}; +pub use log::{GitCommit, GitFileDiff, git_file_diff_for_commit, git_log, git_log_for_file}; pub use pull::{PullResult, git_pull, git_pull_force_reset, git_pull_merge}; pub use push::{PushResult, git_push}; pub use remotes::{GitRemote, git_add_remote, git_remotes, git_rm_remote}; +pub use repository::{GitRepositoryPaths, git_path_is_ignored, git_repository_paths}; pub use reset::git_reset_changes; -pub use status::{GitStatusSummary, git_status}; +pub use restore::{git_restore, git_restore_file_from_commit}; +pub use status::{ + GitBranchInfo, GitStatusSummary, GitWorktreeStatus, git_branch_info, git_status, + git_worktree_status, +}; pub use unstage::git_unstage; diff --git a/crates/yaak-git/src/log.rs b/crates/yaak-git/src/log.rs index 108c0fbd..8b31f80b 100644 --- a/crates/yaak-git/src/log.rs +++ b/crates/yaak-git/src/log.rs @@ -8,6 +8,7 @@ use ts_rs::TS; #[serde(rename_all = "camelCase")] #[ts(export, export_to = "gen_git.ts")] pub struct GitCommit { + pub oid: String, pub author: GitAuthor, pub when: DateTime, pub message: Option, @@ -21,7 +22,23 @@ pub struct GitAuthor { pub email: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_git.ts")] +pub struct GitFileDiff { + pub original: String, + pub modified: String, +} + pub fn git_log(dir: &Path) -> crate::error::Result> { + git_log_inner(dir, None) +} + +pub fn git_log_for_file(dir: &Path, rela_path: &Path) -> crate::error::Result> { + git_log_inner(dir, Some(rela_path)) +} + +fn git_log_inner(dir: &Path, rela_path: Option<&Path>) -> crate::error::Result> { let repo = open_repo(dir)?; // Return empty if empty repo or no head (new repo) @@ -46,8 +63,16 @@ pub fn git_log(dir: &Path) -> crate::error::Result> { .filter_map(|oid| { let oid = filter_try!(oid); let commit = filter_try!(repo.find_commit(oid)); + if let Some(rela_path) = rela_path { + let touches_path = filter_try!(commit_touches_path(&repo, &commit, rela_path)); + if !touches_path { + return None; + } + } + let author = commit.author(); Some(GitCommit { + oid: oid.to_string(), author: GitAuthor { name: author.name().map(|s| s.to_string()), email: author.email().map(|s| s.to_string()), @@ -61,6 +86,53 @@ pub fn git_log(dir: &Path) -> crate::error::Result> { Ok(log) } +pub fn git_file_diff_for_commit( + dir: &Path, + commit_oid: &str, + rela_path: &Path, +) -> crate::error::Result { + let repo = open_repo(dir)?; + let oid = git2::Oid::from_str(commit_oid)?; + let commit = repo.find_commit(oid)?; + let new_tree = commit.tree()?; + let old_tree = if commit.parent_count() > 0 { Some(commit.parent(0)?.tree()?) } else { None }; + + Ok(GitFileDiff { + original: blob_text_at_path(&repo, old_tree.as_ref(), rela_path)?, + modified: blob_text_at_path(&repo, Some(&new_tree), rela_path)?, + }) +} + +fn commit_touches_path( + repo: &git2::Repository, + commit: &git2::Commit, + rela_path: &Path, +) -> crate::error::Result { + let new_tree = commit.tree()?; + let old_tree = if commit.parent_count() > 0 { Some(commit.parent(0)?.tree()?) } else { None }; + + let mut opts = git2::DiffOptions::new(); + opts.pathspec(rela_path); + + let diff = repo.diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut opts))?; + Ok(diff.deltas().len() > 0) +} + +fn blob_text_at_path( + repo: &git2::Repository, + tree: Option<&git2::Tree>, + rela_path: &Path, +) -> crate::error::Result { + let Some(tree) = tree else { + return Ok(String::new()); + }; + let Ok(entry) = tree.get_path(rela_path) else { + return Ok(String::new()); + }; + let blob = entry.to_object(repo)?.peel_to_blob()?; + Ok(String::from_utf8(blob.content().to_vec())?) +} + #[cfg(test)] fn convert_git_time_to_date(_git_time: git2::Time) -> DateTime { DateTime::from_timestamp(0, 0).unwrap() diff --git a/crates/yaak-git/src/repository.rs b/crates/yaak-git/src/repository.rs index c148f07c..6081abf3 100644 --- a/crates/yaak-git/src/repository.rs +++ b/crates/yaak-git/src/repository.rs @@ -1,5 +1,12 @@ use crate::error::Error::{GitRepoNotFound, GitUnknown}; -use std::path::Path; +use crate::error::{Error, Result}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub struct GitRepositoryPaths { + pub workdir: PathBuf, + pub gitdir: PathBuf, +} pub(crate) fn open_repo(dir: &Path) -> crate::error::Result { match git2::Repository::discover(dir) { @@ -8,3 +15,17 @@ pub(crate) fn open_repo(dir: &Path) -> crate::error::Result { Err(e) => Err(GitUnknown(e)), } } + +pub fn git_repository_paths(dir: &Path) -> Result { + let repo = open_repo(dir)?; + let workdir = repo + .workdir() + .ok_or_else(|| Error::GenericError("Git repository does not have a worktree".into()))? + .to_path_buf(); + Ok(GitRepositoryPaths { workdir, gitdir: repo.path().to_path_buf() }) +} + +pub fn git_path_is_ignored(dir: &Path, rela_path: &Path) -> Result { + let repo = open_repo(dir)?; + Ok(repo.status_should_ignore(rela_path)?) +} diff --git a/crates/yaak-git/src/restore.rs b/crates/yaak-git/src/restore.rs new file mode 100644 index 00000000..3d7484f1 --- /dev/null +++ b/crates/yaak-git/src/restore.rs @@ -0,0 +1,76 @@ +use crate::error::Result; +use crate::repository::open_repo; +use log::info; +use std::fs; +use std::path::{Component, Path}; + +pub fn git_restore(dir: &Path, rela_path: &Path) -> Result<()> { + let repo = open_repo(dir)?; + validate_relative_path(rela_path)?; + + let status = repo.status_file(rela_path).ok(); + let is_untracked = status + .is_some_and(|s| s.contains(git2::Status::WT_NEW) || s.contains(git2::Status::INDEX_NEW)); + + info!("Restoring file {rela_path:?} in {dir:?}"); + if is_untracked { + let mut index = repo.index()?; + let _ = index.remove_path(rela_path); + index.write()?; + + let path = repo.workdir().unwrap_or(dir).join(rela_path); + if path.is_dir() { + fs::remove_dir_all(path)?; + } else if path.exists() { + fs::remove_file(path)?; + } + return Ok(()); + } + + let head = repo.head()?; + let commit = head.peel_to_commit()?; + repo.reset_default(Some(commit.as_object()), &[rela_path])?; + + let mut checkout = git2::build::CheckoutBuilder::new(); + checkout.force().path(rela_path); + repo.checkout_head(Some(&mut checkout))?; + + Ok(()) +} + +pub fn git_restore_file_from_commit(dir: &Path, commit_oid: &str, rela_path: &Path) -> Result<()> { + let repo = open_repo(dir)?; + validate_relative_path(rela_path)?; + + let oid = git2::Oid::from_str(commit_oid)?; + let commit = repo.find_commit(oid)?; + let tree = commit.tree()?; + let path = repo.workdir().unwrap_or(dir).join(rela_path); + + info!("Restoring file {rela_path:?} from commit {commit_oid} in {dir:?}"); + if tree.get_path(rela_path).is_err() { + if path.is_dir() { + fs::remove_dir_all(path)?; + } else if path.exists() { + fs::remove_file(path)?; + } + return Ok(()); + } + + let mut checkout = git2::build::CheckoutBuilder::new(); + checkout.force().path(rela_path); + repo.checkout_tree(commit.as_object(), Some(&mut checkout))?; + + Ok(()) +} + +fn validate_relative_path(path: &Path) -> Result<()> { + let is_safe = !path.as_os_str().is_empty() + && !path.is_absolute() + && path.components().all(|c| matches!(c, Component::Normal(_))); + if is_safe { + Ok(()) + } else { + Err(crate::error::Error::GenericError(format!("Invalid restore path {}", path.display()))) + } +} diff --git a/crates/yaak-git/src/status.rs b/crates/yaak-git/src/status.rs index b2dad85b..c07c218e 100644 --- a/crates/yaak-git/src/status.rs +++ b/crates/yaak-git/src/status.rs @@ -22,6 +22,20 @@ pub struct GitStatusSummary { pub behind: u32, } +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_git.ts")] +pub struct GitBranchInfo { + pub path: String, + pub head_ref: Option, + pub head_ref_shorthand: Option, + pub origins: Vec, + pub local_branches: Vec, + pub remote_branches: Vec, + pub ahead: u32, + pub behind: u32, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "gen_git.ts")] @@ -33,6 +47,23 @@ pub struct GitStatusEntry { pub next: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_git.ts")] +pub struct GitWorktreeStatus { + pub entries: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_git.ts")] +pub struct GitWorktreeStatusEntry { + pub rela_path: String, + pub model_id: Option, + pub status: GitStatus, + pub staged: bool, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "snake_case")] #[ts(export, export_to = "gen_git.ts")] @@ -46,31 +77,43 @@ pub enum GitStatus { TypeChange, } +pub fn git_worktree_status(dir: &Path) -> crate::error::Result { + let repo = open_repo(dir)?; + let mut opts = git2::StatusOptions::new(); + opts.include_ignored(false) + .include_untracked(true) + .recurse_untracked_dirs(true) + .include_unmodified(false); + + let mut entries = Vec::new(); + for entry in repo.statuses(Some(&mut opts))?.into_iter() { + let Some(rela_path) = entry.path() else { + continue; + }; + let Some((status, staged)) = git_status_from_raw(entry.status()) else { + continue; + }; + + entries.push(GitWorktreeStatusEntry { + rela_path: rela_path.to_string(), + model_id: model_id_from_rela_path(Path::new(rela_path)), + status, + staged, + }); + } + + Ok(GitWorktreeStatus { entries }) +} + +pub fn git_branch_info(dir: &Path) -> crate::error::Result { + let repo = open_repo(dir)?; + git_branch_info_for_repo(&repo, dir) +} + pub fn git_status(dir: &Path) -> crate::error::Result { let repo = open_repo(dir)?; - let (head_tree, head_ref, head_ref_shorthand) = match repo.head() { - Ok(head) => { - let tree = head.peel_to_tree().ok(); - let head_ref_shorthand = head.shorthand().map(|s| s.to_string()); - let head_ref = head.name().map(|s| s.to_string()); - - (tree, head_ref, head_ref_shorthand) - } - Err(_) => { - // For "unborn" repos, reading from HEAD is the only way to get the branch name - // See https://github.com/starship/starship/pull/1336 - let head_path = repo.path().join("HEAD"); - let head_ref = fs::read_to_string(&head_path) - .ok() - .unwrap_or_default() - .lines() - .next() - .map(|s| s.trim_start_matches("ref:").trim().to_string()); - let head_ref_shorthand = - head_ref.clone().map(|r| r.split('/').last().unwrap_or("unknown").to_string()); - (None, head_ref, head_ref_shorthand) - } - }; + let branch_info = git_branch_info_for_repo(&repo, dir)?; + let head_tree = repo.head().ok().and_then(|head| head.peel_to_tree().ok()); let mut opts = git2::StatusOptions::new(); opts.include_ignored(false) @@ -83,51 +126,8 @@ pub fn git_status(dir: &Path) -> crate::error::Result { let mut entries: Vec = Vec::new(); for entry in repo.statuses(Some(&mut opts))?.into_iter() { let rela_path = entry.path().unwrap().to_string(); - let status = entry.status(); - let index_status = match status { - // Note: order matters here, since we're checking a bitmap! - s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict, - s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Untracked, - s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified, - s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Removed, - s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed, - s if s.contains(git2::Status::INDEX_TYPECHANGE) => GitStatus::TypeChange, - s if s.contains(git2::Status::CURRENT) => GitStatus::Current, - s => { - warn!("Unknown index status {s:?}"); - continue; - } - }; - - let worktree_status = match status { - // Note: order matters here, since we're checking a bitmap! - s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict, - s if s.contains(git2::Status::WT_NEW) => GitStatus::Untracked, - s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified, - s if s.contains(git2::Status::WT_DELETED) => GitStatus::Removed, - s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed, - s if s.contains(git2::Status::WT_TYPECHANGE) => GitStatus::TypeChange, - s if s.contains(git2::Status::CURRENT) => GitStatus::Current, - s => { - warn!("Unknown worktree status {s:?}"); - continue; - } - }; - - let status = if index_status == GitStatus::Current { - worktree_status.clone() - } else { - index_status.clone() - }; - - let staged = if index_status == GitStatus::Current && worktree_status == GitStatus::Current - { - // No change, so can't be added - false - } else if index_status != GitStatus::Current { - true - } else { - false + let Some((status, staged)) = git_status_from_raw(entry.status()) else { + continue; }; // Get previous content from Git, if it's in there @@ -158,9 +158,27 @@ pub fn git_status(dir: &Path) -> crate::error::Result { }) } + Ok(GitStatusSummary { + entries, + path: branch_info.path, + head_ref: branch_info.head_ref, + head_ref_shorthand: branch_info.head_ref_shorthand, + origins: branch_info.origins, + local_branches: branch_info.local_branches, + remote_branches: branch_info.remote_branches, + ahead: branch_info.ahead, + behind: branch_info.behind, + }) +} + +fn git_branch_info_for_repo( + repo: &git2::Repository, + dir: &Path, +) -> crate::error::Result { + let (head_ref, head_ref_shorthand) = git_head_refs(repo); let origins = repo.remotes()?.into_iter().filter_map(|o| Some(o?.to_string())).collect(); - let local_branches = local_branch_names(&repo)?; - let remote_branches = remote_branch_names(&repo)?; + let local_branches = local_branch_names(repo)?; + let remote_branches = remote_branch_names(repo)?; // Compute ahead/behind relative to remote tracking branch let (ahead, behind) = (|| -> Option<(usize, usize)> { @@ -174,15 +192,85 @@ pub fn git_status(dir: &Path) -> crate::error::Result { })() .unwrap_or((0, 0)); - Ok(GitStatusSummary { - entries, - origins, + Ok(GitBranchInfo { path: dir.to_string_lossy().to_string(), head_ref, head_ref_shorthand, + origins, local_branches, remote_branches, ahead: ahead as u32, behind: behind as u32, }) } + +fn git_head_refs(repo: &git2::Repository) -> (Option, Option) { + match repo.head() { + Ok(head) => { + let head_ref = head.name().map(|s| s.to_string()); + let head_ref_shorthand = head.shorthand().map(|s| s.to_string()); + (head_ref, head_ref_shorthand) + } + Err(_) => { + // For "unborn" repos, reading from HEAD is the only way to get the branch name + // See https://github.com/starship/starship/pull/1336 + let head_path = repo.path().join("HEAD"); + let head_ref = fs::read_to_string(&head_path) + .ok() + .unwrap_or_default() + .lines() + .next() + .map(|s| s.trim_start_matches("ref:").trim().to_string()); + let head_ref_shorthand = + head_ref.clone().map(|r| r.split('/').last().unwrap_or("unknown").to_string()); + (head_ref, head_ref_shorthand) + } + } +} + +fn git_status_from_raw(status: git2::Status) -> Option<(GitStatus, bool)> { + let index_status = match status { + // Note: order matters here, since we're checking a bitmap! + s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict, + s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Untracked, + s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified, + s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Removed, + s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed, + s if s.contains(git2::Status::INDEX_TYPECHANGE) => GitStatus::TypeChange, + s if s.contains(git2::Status::CURRENT) => GitStatus::Current, + s => { + warn!("Unknown index status {s:?}"); + return None; + } + }; + + let worktree_status = match status { + // Note: order matters here, since we're checking a bitmap! + s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict, + s if s.contains(git2::Status::WT_NEW) => GitStatus::Untracked, + s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified, + s if s.contains(git2::Status::WT_DELETED) => GitStatus::Removed, + s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed, + s if s.contains(git2::Status::WT_TYPECHANGE) => GitStatus::TypeChange, + s if s.contains(git2::Status::CURRENT) => GitStatus::Current, + s => { + warn!("Unknown worktree status {s:?}"); + return None; + } + }; + + let status = + if index_status == GitStatus::Current { worktree_status } else { index_status.clone() }; + let staged = index_status != GitStatus::Current; + + Some((status, staged)) +} + +fn model_id_from_rela_path(path: &Path) -> Option { + let ext = path.extension()?.to_str()?; + if ext != "yaml" && ext != "yml" && ext != "json" { + return None; + } + + path.file_stem()?.to_str()?.strip_prefix("yaak.").map(String::from) +} diff --git a/crates/yaak-http/src/types.rs b/crates/yaak-http/src/types.rs index ed113752..0794df21 100644 --- a/crates/yaak-http/src/types.rs +++ b/crates/yaak-http/src/types.rs @@ -304,7 +304,10 @@ async fn build_binary_body( })) } -fn build_text_body(body: &BTreeMap, body_type: &str) -> Option { +fn build_text_body( + body: &BTreeMap, + body_type: &str, +) -> Option { let text = get_str_map(body, "text"); if text.is_empty() { return None; diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs index d7efe1e5..1b47ba79 100644 --- a/crates/yaak-models/src/models.rs +++ b/crates/yaak-models/src/models.rs @@ -16,8 +16,8 @@ use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::str::FromStr; use ts_rs::TS; +use yaak_database::{Result as DbResult, UpdateSource}; pub use yaak_database::{UpsertModelInfo, upsert_date}; -use yaak_database::{UpdateSource, Result as DbResult}; #[macro_export] macro_rules! impl_model { @@ -2526,4 +2526,3 @@ impl AnyModel { } } } - diff --git a/crates/yaak-models/src/queries/folders.rs b/crates/yaak-models/src/queries/folders.rs index 20d60ce3..702cae21 100644 --- a/crates/yaak-models/src/queries/folders.rs +++ b/crates/yaak-models/src/queries/folders.rs @@ -1,5 +1,5 @@ -use crate::connection_or_tx::ConnectionOrTx; use crate::client_db::ClientDb; +use crate::connection_or_tx::ConnectionOrTx; use crate::error::Result; use crate::models::{ Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, diff --git a/crates/yaak-models/src/queries/plugin_key_values.rs b/crates/yaak-models/src/queries/plugin_key_values.rs index 9d5c664a..e01e471c 100644 --- a/crates/yaak-models/src/queries/plugin_key_values.rs +++ b/crates/yaak-models/src/queries/plugin_key_values.rs @@ -16,7 +16,10 @@ impl<'a> ClientDb<'a> { .add(Expr::col(PluginKeyValueIden::Key).eq(key)), ) .build_rusqlite(SqliteQueryBuilder); - self.conn().resolve().query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok() + self.conn() + .resolve() + .query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()) + .ok() } pub fn set_plugin_key_value( diff --git a/crates/yaak-models/src/util.rs b/crates/yaak-models/src/util.rs index a05a4ff1..62cb7bd7 100644 --- a/crates/yaak-models/src/util.rs +++ b/crates/yaak-models/src/util.rs @@ -10,7 +10,9 @@ use std::collections::BTreeMap; use ts_rs::TS; use yaak_core::WorkspaceContext; -pub use yaak_database::{ModelChangeEvent, generate_id, generate_id_of_length, generate_prefixed_id}; +pub use yaak_database::{ + ModelChangeEvent, generate_id, generate_id_of_length, generate_prefixed_id, +}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] diff --git a/crates/yaak-proxy/src/body.rs b/crates/yaak-proxy/src/body.rs index 216206de..5eef207a 100644 --- a/crates/yaak-proxy/src/body.rs +++ b/crates/yaak-proxy/src/body.rs @@ -79,10 +79,9 @@ where let len = data.len(); self.bytes_count += len as u64; self.chunks.push(data.clone()); - let _ = self.event_tx.send(ProxyEvent::ResponseBodyChunk { - id: self.request_id, - bytes: len, - }); + let _ = self + .event_tx + .send(ProxyEvent::ResponseBodyChunk { id: self.request_id, bytes: len }); } Poll::Ready(Some(Ok(frame))) } diff --git a/crates/yaak-proxy/src/cert.rs b/crates/yaak-proxy/src/cert.rs index 7636057c..e9105e02 100644 --- a/crates/yaak-proxy/src/cert.rs +++ b/crates/yaak-proxy/src/cert.rs @@ -18,23 +18,14 @@ impl CertificateAuthority { params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); params.key_usages.push(KeyUsagePurpose::KeyCertSign); params.key_usages.push(KeyUsagePurpose::CrlSign); - params - .distinguished_name - .push(rcgen::DnType::CommonName, "Debug Proxy CA"); - params - .distinguished_name - .push(rcgen::DnType::OrganizationName, "Debug Proxy"); + params.distinguished_name.push(rcgen::DnType::CommonName, "Debug Proxy CA"); + params.distinguished_name.push(rcgen::DnType::OrganizationName, "Debug Proxy"); let key = KeyPair::generate()?; let ca_cert = params.self_signed(&key)?; let ca_cert_der = ca_cert.der().clone(); - Ok(Self { - ca_cert, - ca_cert_der, - ca_key: key, - cache: Mutex::new(HashMap::new()), - }) + Ok(Self { ca_cert, ca_cert_der, ca_key: key, cache: Mutex::new(HashMap::new()) }) } pub fn ca_pem(&self) -> String { @@ -53,9 +44,7 @@ impl CertificateAuthority { } let mut params = CertificateParams::new(vec![domain.to_string()])?; - params - .distinguished_name - .push(rcgen::DnType::CommonName, domain); + params.distinguished_name.push(rcgen::DnType::CommonName, domain); let leaf_key = KeyPair::generate()?; let leaf_cert = params.signed_by(&leaf_key, &self.ca_cert, &self.ca_key)?; @@ -63,20 +52,18 @@ impl CertificateAuthority { let cert_der = leaf_cert.der().clone(); let key_der = leaf_key.serialize_der(); - let mut config = ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) - .with_safe_default_protocol_versions()? - .with_no_client_auth() - .with_single_cert( - vec![cert_der, self.ca_cert_der.clone()], - PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_der)), - )?; + let mut config = + ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) + .with_safe_default_protocol_versions()? + .with_no_client_auth() + .with_single_cert( + vec![cert_der, self.ca_cert_der.clone()], + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_der)), + )?; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; let config = Arc::new(config); - self.cache - .lock() - .unwrap() - .insert(domain.to_string(), config.clone()); + self.cache.lock().unwrap().insert(domain.to_string(), config.clone()); Ok(config) } } diff --git a/crates/yaak-proxy/src/connection.rs b/crates/yaak-proxy/src/connection.rs index 77e6ee6d..9719876e 100644 --- a/crates/yaak-proxy/src/connection.rs +++ b/crates/yaak-proxy/src/connection.rs @@ -1,5 +1,5 @@ -use std::sync::mpsc as std_mpsc; use std::sync::Arc; +use std::sync::mpsc as std_mpsc; use hyper::server::conn::http1; use hyper::service::service_fn; diff --git a/crates/yaak-proxy/src/lib.rs b/crates/yaak-proxy/src/lib.rs index 80f2b82a..a83a2d7c 100644 --- a/crates/yaak-proxy/src/lib.rs +++ b/crates/yaak-proxy/src/lib.rs @@ -4,9 +4,9 @@ mod connection; mod request; use std::net::SocketAddr; +use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::sync::mpsc as std_mpsc; -use std::sync::Arc; use cert::CertificateAuthority; use tokio::net::TcpListener; @@ -27,7 +27,11 @@ pub enum ProxyEvent { http_version: String, }, /// A request header sent to the upstream server. - RequestHeader { id: u64, name: String, value: String }, + RequestHeader { + id: u64, + name: String, + value: String, + }, /// The full request body (buffered before forwarding). RequestBody { id: u64, body: Vec }, /// Response headers received from upstream. @@ -38,7 +42,11 @@ pub enum ProxyEvent { elapsed_ms: u64, }, /// A response header received from the upstream server. - ResponseHeader { id: u64, name: String, value: String }, + ResponseHeader { + id: u64, + name: String, + value: String, + }, /// A chunk of the response body was received (emitted per-frame). ResponseBodyChunk { id: u64, bytes: usize }, /// The response body stream has completed. diff --git a/crates/yaak-proxy/src/request.rs b/crates/yaak-proxy/src/request.rs index 7285af5c..1de6bb63 100644 --- a/crates/yaak-proxy/src/request.rs +++ b/crates/yaak-proxy/src/request.rs @@ -63,10 +63,7 @@ fn emit_request_events( }); } if let Some(body) = body { - let _ = tx.send(ProxyEvent::RequestBody { - id, - body: body.clone(), - }); + let _ = tx.send(ProxyEvent::RequestBody { id, body: body.clone() }); } } @@ -123,22 +120,13 @@ async fn handle_http( let http_version = version_str(req.version()); let start = Instant::now(); - let _ = event_tx.send(ProxyEvent::RequestStart { - id, - method, - url: uri.clone(), - http_version, - }); + let _ = event_tx.send(ProxyEvent::RequestStart { id, method, url: uri.clone(), http_version }); let client: Client<_, Full> = Client::builder(TokioExecutor::new()).build_http(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); - let request_body = if body_bytes.is_empty() { - None - } else { - Some(body_bytes.to_vec()) - }; + let request_body = if body_bytes.is_empty() { None } else { Some(body_bytes.to_vec()) }; emit_request_events(&event_tx, id, &parts.headers, &request_body); let outgoing_req = Request::from_parts(parts, Full::new(body_bytes)); @@ -148,16 +136,10 @@ async fn handle_http( emit_response_events(&event_tx, id, &resp, &start); let (parts, body) = resp.into_parts(); - Ok(Response::from_parts( - parts, - measured_incoming(body, id, start, event_tx), - )) + Ok(Response::from_parts(parts, measured_incoming(body, id, start, event_tx))) } Err(e) => { - let _ = event_tx.send(ProxyEvent::Error { - id, - error: e.to_string(), - }); + let _ = event_tx.send(ProxyEvent::Error { id, error: e.to_string() }); Err(Box::new(e) as Box) } } @@ -168,11 +150,7 @@ async fn handle_connect( event_tx: std_mpsc::Sender, ca: Arc, ) -> Result, Box> { - let authority = req - .uri() - .authority() - .map(|a| a.to_string()) - .unwrap_or_default(); + let authority = req.uri().authority().map(|a| a.to_string()).unwrap_or_default(); let (host, port) = parse_host_port(&authority); let server_config = ca.server_config(&host)?; @@ -189,10 +167,7 @@ async fn handle_connect( } }; - let tls_stream = match acceptor - .accept(hyper_util::rt::TokioIo::new(upgraded)) - .await - { + let tls_stream = match acceptor.accept(hyper_util::rt::TokioIo::new(upgraded)).await { Ok(s) => s, Err(e) => { eprintln!("TLS accept failed for {host}: {e}"); @@ -203,10 +178,7 @@ async fn handle_connect( let tx = event_tx.clone(); let host_for_requests = host.clone(); let mut builder = auto::Builder::new(TokioExecutor::new()); - builder - .http1() - .preserve_header_case(true) - .title_case_headers(true); + builder.http1().preserve_header_case(true).title_case_headers(true); if let Err(e) = builder .serve_connection_with_upgrades( hyper_util::rt::TokioIo::new(tls_stream), @@ -271,20 +243,12 @@ async fn forward_https( let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); let method = req.method().to_string(); let http_version = version_str(req.version()); - let path = req - .uri() - .path_and_query() - .map(|pq| pq.to_string()) - .unwrap_or_else(|| "/".into()); + let path = req.uri().path_and_query().map(|pq| pq.to_string()).unwrap_or_else(|| "/".into()); let uri_str = format!("https://{host}{path}"); let start = Instant::now(); - let _ = event_tx.send(ProxyEvent::RequestStart { - id, - method, - url: uri_str.clone(), - http_version, - }); + let _ = + event_tx.send(ProxyEvent::RequestStart { id, method, url: uri_str.clone(), http_version }); // Connect to upstream with TLS let tcp_stream = TcpStream::connect(target_addr).await?; @@ -305,18 +269,13 @@ async fn forward_https( let server_name = ServerName::try_from(host.to_string())?; let tls_stream = connector.connect(server_name, tcp_stream).await?; - let negotiated_h2 = tls_stream - .get_ref() - .1 - .alpn_protocol() - .map_or(false, |p| p == b"h2"); + let negotiated_h2 = tls_stream.get_ref().1.alpn_protocol().map_or(false, |p| p == b"h2"); let io = hyper_util::rt::TokioIo::new(tls_stream); let mut sender = if negotiated_h2 { - let (sender, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new()) - .handshake(io) - .await?; + let (sender, conn) = + hyper::client::conn::http2::Builder::new(TokioExecutor::new()).handshake(io).await?; tokio::spawn(async move { if let Err(e) = conn.await { eprintln!("Upstream h2 connection error: {e}"); @@ -340,11 +299,7 @@ async fn forward_https( // Capture request metadata let (mut parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); - let request_body = if body_bytes.is_empty() { - None - } else { - Some(body_bytes.to_vec()) - }; + let request_body = if body_bytes.is_empty() { None } else { Some(body_bytes.to_vec()) }; emit_request_events(&event_tx, id, &parts.headers, &request_body); if negotiated_h2 { @@ -365,16 +320,10 @@ async fn forward_https( emit_response_events(&event_tx, id, &resp, &start); let (parts, body) = resp.into_parts(); - Ok(Response::from_parts( - parts, - measured_incoming(body, id, start, event_tx), - )) + Ok(Response::from_parts(parts, measured_incoming(body, id, start, event_tx))) } Err(e) => { - let _ = event_tx.send(ProxyEvent::Error { - id, - error: e.to_string(), - }); + let _ = event_tx.send(ProxyEvent::Error { id, error: e.to_string() }); Err(Box::new(e) as Box) } } diff --git a/crates/yaak-templates/src/lib.rs b/crates/yaak-templates/src/lib.rs index db5c4e14..1fc5a6ea 100644 --- a/crates/yaak-templates/src/lib.rs +++ b/crates/yaak-templates/src/lib.rs @@ -1,9 +1,9 @@ pub mod error; pub mod escape; pub mod format_json; -pub mod strip_json_comments; pub mod parser; pub mod renderer; +pub mod strip_json_comments; pub mod wasm; pub use parser::*; diff --git a/crates/yaak-templates/src/strip_json_comments.rs b/crates/yaak-templates/src/strip_json_comments.rs index ade19c8c..5941f10e 100644 --- a/crates/yaak-templates/src/strip_json_comments.rs +++ b/crates/yaak-templates/src/strip_json_comments.rs @@ -113,11 +113,8 @@ pub fn strip_json_comments(text: &str) -> String { } // Remove lines that are now empty (were comment-only lines) - let result = result - .lines() - .filter(|line| !line.trim().is_empty()) - .collect::>() - .join("\n"); + let result = + result.lines().filter(|line| !line.trim().is_empty()).collect::>().join("\n"); // Remove trailing commas before } or ] strip_trailing_commas(&result) @@ -192,10 +189,12 @@ mod tests { #[test] fn test_trailing_line_comment() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ "foo": "bar", // this is a comment "baz": 123 -}"#), +}"# + ), r#"{ "foo": "bar", "baz": 123 @@ -206,10 +205,12 @@ mod tests { #[test] fn test_whole_line_comment() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ // this is a comment "foo": "bar" -}"#), +}"# + ), r#"{ "foo": "bar" }"# @@ -219,9 +220,11 @@ mod tests { #[test] fn test_inline_block_comment() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ "foo": /* a comment */ "bar" -}"#), +}"# + ), r#"{ "foo": "bar" }"# @@ -231,10 +234,12 @@ mod tests { #[test] fn test_whole_line_block_comment() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ /* a comment */ "foo": "bar" -}"#), +}"# + ), r#"{ "foo": "bar" }"# @@ -244,12 +249,14 @@ mod tests { #[test] fn test_multiline_block_comment() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ /** * Hello World! */ "foo": "bar" -}"#), +}"# + ), r#"{ "foo": "bar" }"# @@ -276,12 +283,14 @@ mod tests { #[test] fn test_multiple_comments() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ // first comment "foo": "bar", // trailing /* block */ "baz": 123 -}"#), +}"# + ), r#"{ "foo": "bar", "baz": 123 @@ -292,10 +301,12 @@ mod tests { #[test] fn test_trailing_comma_after_comment_removed() { assert_eq!( - strip_json_comments(r#"{ + strip_json_comments( + r#"{ "a": "aaa", // "b": "bbb" -}"#), +}"# + ), r#"{ "a": "aaa" }"# @@ -304,10 +315,7 @@ mod tests { #[test] fn test_trailing_comma_in_array() { - assert_eq!( - strip_json_comments(r#"[1, 2, /* 3 */]"#), - r#"[1, 2]"# - ); + assert_eq!(strip_json_comments(r#"[1, 2, /* 3 */]"#), r#"[1, 2]"#); } #[test] diff --git a/crates/yaak/src/render.rs b/crates/yaak/src/render.rs index 75015ae7..5523c9ce 100644 --- a/crates/yaak/src/render.rs +++ b/crates/yaak/src/render.rs @@ -2,7 +2,9 @@ use log::info; use serde_json::Value; use std::collections::BTreeMap; use yaak_http::path_placeholders::apply_path_placeholders; -use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter}; +use yaak_models::models::{ + Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter, +}; use yaak_models::render::make_vars_hashmap; use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; diff --git a/package-lock.json b/package-lock.json index eeef35d3..67f7adc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ }, "devDependencies": { "@rolldown/plugin-babel": "^0.2.3", - "@tauri-apps/cli": "^2.9.6", + "@tauri-apps/cli": "^2.11.1", "@types/babel__core": "^7.20.5", "@vitejs/plugin-react": "^6.0.1", "@yaakapp/cli": "^0.5.1", @@ -119,14 +119,14 @@ "@tanstack/react-query": "^5.90.5", "@tanstack/react-router": "^1.133.13", "@tanstack/react-virtual": "^3.13.12", - "@tauri-apps/api": "^2.9.1", + "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", - "@tauri-apps/plugin-dialog": "^2.4.2", - "@tauri-apps/plugin-fs": "^2.4.4", - "@tauri-apps/plugin-log": "^2.7.1", - "@tauri-apps/plugin-opener": "^2.5.2", + "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", + "@tauri-apps/plugin-log": "^2.8.0", + "@tauri-apps/plugin-opener": "^2.5.4", "@tauri-apps/plugin-os": "^2.3.2", - "@tauri-apps/plugin-shell": "^2.3.3", + "@tauri-apps/plugin-shell": "^2.3.5", "buffer": "^6.0.3", "classnames": "^2.5.1", "cm6-graphql": "^0.2.1", @@ -140,6 +140,7 @@ "hexy": "^0.3.5", "history": "^5.3.0", "jotai": "^2.18.0", + "jotai-family": "^1.0.1", "js-md5": "^0.8.3", "lucide-react": "^0.525.0", "mime": "^4.0.4", @@ -240,7 +241,7 @@ "version": "1.0.0", "dependencies": { "@tanstack/react-query": "^5.90.5", - "@tauri-apps/api": "^2.9.1", + "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-os": "^2.3.2", "@yaakapp-internal/model-store": "^1.0.0", "@yaakapp-internal/proxy-lib": "^1.0.0", @@ -248,6 +249,7 @@ "@yaakapp-internal/ui": "^1.0.0", "classnames": "^2.5.1", "jotai": "^2.18.0", + "jotai-family": "^1.0.1", "motion": "^12.4.7", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -4173,9 +4175,9 @@ } }, "node_modules/@tauri-apps/api": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz", - "integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", @@ -4183,9 +4185,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz", - "integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz", + "integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -4199,23 +4201,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.9.6", - "@tauri-apps/cli-darwin-x64": "2.9.6", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", - "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", - "@tauri-apps/cli-linux-arm64-musl": "2.9.6", - "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", - "@tauri-apps/cli-linux-x64-gnu": "2.9.6", - "@tauri-apps/cli-linux-x64-musl": "2.9.6", - "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", - "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", - "@tauri-apps/cli-win32-x64-msvc": "2.9.6" + "@tauri-apps/cli-darwin-arm64": "2.11.1", + "@tauri-apps/cli-darwin-x64": "2.11.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.1", + "@tauri-apps/cli-linux-arm64-musl": "2.11.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-musl": "2.11.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.1", + "@tauri-apps/cli-win32-x64-msvc": "2.11.1" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz", - "integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", + "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", "cpu": [ "arm64" ], @@ -4230,9 +4232,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz", - "integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz", + "integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==", "cpu": [ "x64" ], @@ -4247,9 +4249,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz", - "integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz", + "integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==", "cpu": [ "arm" ], @@ -4264,9 +4266,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz", - "integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz", + "integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==", "cpu": [ "arm64" ], @@ -4281,9 +4283,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz", - "integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz", + "integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==", "cpu": [ "arm64" ], @@ -4298,9 +4300,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz", - "integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz", + "integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==", "cpu": [ "riscv64" ], @@ -4315,9 +4317,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz", - "integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz", + "integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==", "cpu": [ "x64" ], @@ -4332,9 +4334,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz", - "integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz", + "integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==", "cpu": [ "x64" ], @@ -4349,9 +4351,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz", - "integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz", + "integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==", "cpu": [ "arm64" ], @@ -4366,9 +4368,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz", - "integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz", + "integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==", "cpu": [ "ia32" ], @@ -4383,9 +4385,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz", - "integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz", + "integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==", "cpu": [ "x64" ], @@ -4409,21 +4411,21 @@ } }, "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.5.0.tgz", - "integrity": "sha512-I0R0ygwRd9AN8Wj5GnzCogOlqu2+OWAtBd0zEC4+kQCI32fRowIyuhPCBoUv4h/lQt2bM39kHlxPHD5vDcFjiA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.8.0" + "@tauri-apps/api": "^2.11.0" } }, "node_modules/@tauri-apps/plugin-fs": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz", - "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.1.tgz", + "integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.8.0" + "@tauri-apps/api": "^2.11.0" } }, "node_modules/@tauri-apps/plugin-log": { @@ -4436,12 +4438,12 @@ } }, "node_modules/@tauri-apps/plugin-opener": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", - "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.8.0" + "@tauri-apps/api": "^2.11.0" } }, "node_modules/@tauri-apps/plugin-os": { @@ -4454,12 +4456,12 @@ } }, "node_modules/@tauri-apps/plugin-shell": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.4.tgz", - "integrity": "sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", + "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.8.0" + "@tauri-apps/api": "^2.10.1" } }, "node_modules/@types/aws4": { @@ -7927,9 +7929,9 @@ "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -9733,6 +9735,18 @@ } } }, + "node_modules/jotai-family": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jotai-family/-/jotai-family-1.0.1.tgz", + "integrity": "sha512-Zb/79GNDhC/z82R+6qTTpeKW4l4H6ZCApfF5W8G4SH37E4mhbysU7r8DkP0KX94hWvjB/6lt/97nSr3wB+64Zg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "jotai": ">=2.9.0" + } + }, "node_modules/js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -16941,14 +16955,17 @@ "name": "@yaakapp-internal/theme", "version": "1.0.0", "dependencies": { - "@tauri-apps/api": "^2.9.1", + "@tauri-apps/api": "^2.11.0", "@yaakapp-internal/plugins": "^1.0.0", "parse-color": "^1.0.0" } }, "packages/ui": { "name": "@yaakapp-internal/ui", - "version": "1.0.0" + "version": "1.0.0", + "dependencies": { + "jotai-family": "^1.0.1" + } }, "plugins-external/faker": { "name": "@yaak/faker", diff --git a/package.json b/package.json index 29fadeee..8f2265b4 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ }, "devDependencies": { "@rolldown/plugin-babel": "^0.2.3", - "@tauri-apps/cli": "^2.9.6", + "@tauri-apps/cli": "^2.11.1", "@types/babel__core": "^7.20.5", "@vitejs/plugin-react": "^6.0.1", "@yaakapp/cli": "^0.5.1", diff --git a/packages/common-lib/eagerDebounceAsync.ts b/packages/common-lib/eagerDebounceAsync.ts new file mode 100644 index 00000000..09e2c2b1 --- /dev/null +++ b/packages/common-lib/eagerDebounceAsync.ts @@ -0,0 +1,36 @@ +export function eagerDebounceAsync(fn: () => Promise, delay: number) { + let timer: ReturnType | null = null; + let inFlight: Promise | null = null; + let runAfterInFlight = false; + + const run = async () => { + if (inFlight != null) { + runAfterInFlight = true; + return; + } + + runAfterInFlight = false; + inFlight = fn() + .catch(console.error) + .finally(() => { + inFlight = null; + if (runAfterInFlight && timer == null) { + void run(); + } + }); + await inFlight; + }; + + return () => { + if (timer == null) { + void run(); + } else { + clearTimeout(timer); + } + + timer = setTimeout(() => { + timer = null; + void run(); + }, delay); + }; +} diff --git a/packages/common-lib/index.ts b/packages/common-lib/index.ts index 01983b42..245e43a4 100644 --- a/packages/common-lib/index.ts +++ b/packages/common-lib/index.ts @@ -1,3 +1,4 @@ export * from "./debounce"; +export * from "./eagerDebounceAsync"; export * from "./formatSize"; export * from "./templateFunction"; diff --git a/packages/theme/package.json b/packages/theme/package.json index bb125320..8ece0029 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -6,7 +6,7 @@ "main": "src/index.ts", "types": "src/index.ts", "dependencies": { - "@tauri-apps/api": "^2.9.1", + "@tauri-apps/api": "^2.11.0", "@yaakapp-internal/plugins": "^1.0.0", "parse-color": "^1.0.0" } diff --git a/packages/ui/package.json b/packages/ui/package.json index c0b8a1a5..283be298 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,5 +4,8 @@ "private": true, "type": "module", "main": "src/index.ts", - "types": "src/index.ts" + "types": "src/index.ts", + "dependencies": { + "jotai-family": "^1.0.1" + } } diff --git a/packages/ui/src/components/tree/TreeItem.tsx b/packages/ui/src/components/tree/TreeItem.tsx index 6a64195c..61375662 100644 --- a/packages/ui/src/components/tree/TreeItem.tsx +++ b/packages/ui/src/components/tree/TreeItem.tsx @@ -81,7 +81,7 @@ function TreeItem_({ const isLastSelected = useAtomValue(isLastFocusedFamily({ treeId, itemId: node.item.id })); const [editing, setEditing] = useState(false); const [dropHover, setDropHover] = useState(null); - const startedHoverTimeout = useRef(undefined); + const startedHoverTimeout = useRef>(undefined); const handle = useMemo( () => ({ focus: () => { @@ -141,7 +141,13 @@ function TreeItem_({ const handleSubmitNameEdit = useCallback( async (el: HTMLInputElement) => { - getEditOptions?.(node.item).onChange(node.item, el.value); + const editOptions = getEditOptions?.(node.item); + if (editOptions == null || el.value === editOptions.defaultValue) { + setEditing(false); + return; + } + + editOptions.onChange(node.item, el.value); onClick?.(node.item, { shiftKey: false, ctrlKey: false, metaKey: false }); // Slight delay for the model to propagate to the local store setTimeout(() => setEditing(false), 200); diff --git a/packages/ui/src/components/tree/atoms.ts b/packages/ui/src/components/tree/atoms.ts index 8a6a64b5..d7070d11 100644 --- a/packages/ui/src/components/tree/atoms.ts +++ b/packages/ui/src/components/tree/atoms.ts @@ -1,5 +1,6 @@ import { atom } from "jotai"; -import { atomFamily, selectAtom } from "jotai/utils"; +import { atomFamily } from "jotai-family"; +import { selectAtom } from "jotai/utils"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const selectedIdsFamily = atomFamily((_treeId: string) => { diff --git a/vite.config.ts b/vite.config.ts index 2b8e5e29..421e45f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,9 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ + staged: { + "*": "vp check --fix", + }, lint: { ignorePatterns: ["npm/**", "crates/yaak-templates/pkg/**", "**/bindings/gen_*.ts"], options: {