diff --git a/Cargo.lock b/Cargo.lock index 79507aae3..a57642af3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -113,6 +128,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arc-swap" version = "1.9.2" @@ -188,6 +212,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -297,6 +330,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -309,15 +348,30 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -372,12 +426,48 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytecount" version = "0.6.9" @@ -407,6 +497,9 @@ name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] [[package]] name = "cairo-rs" @@ -433,6 +526,58 @@ dependencies = [ "system-deps", ] +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.3+spec-1.1.0", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cbc" version = "0.1.2" @@ -454,6 +599,23 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -574,6 +736,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.12.0" @@ -583,6 +768,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -599,6 +794,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -626,6 +845,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.17" @@ -641,6 +866,33 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -651,12 +903,113 @@ dependencies = [ "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf 0.11.3", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.3", +] + [[package]] name = "ctor" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core 0.24.1", + "darling_macro 0.24.1", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.3", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core 0.24.1", + "quote", + "syn 3.0.3", +] + [[package]] name = "data-encoding" version = "2.11.1" @@ -712,20 +1065,82 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] -name = "deranged" -version = "0.5.8" +name = "defmt" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] [[package]] -name = "diff" -version = "0.1.13" +name = "defmt-macros" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ @@ -762,6 +1177,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.1", + "block2", + "libc", "objc2", ] @@ -776,11 +1193,76 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set 0.8.0", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "dpi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] [[package]] name = "dunce" @@ -788,6 +1270,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.16.0" @@ -803,6 +1291,26 @@ dependencies = [ "serde", ] +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.3+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + [[package]] name = "envoy-types" version = "0.6.1" @@ -820,6 +1328,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -830,13 +1349,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fancy-regex" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] @@ -866,12 +1404,35 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "finl_unicode" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.10" @@ -912,7 +1473,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -921,6 +1503,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1102,6 +1690,47 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1232,6 +1861,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1307,13 +1942,30 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -1343,6 +1995,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1361,6 +2019,16 @@ dependencies = [ "digest", ] +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "http" version = "1.4.2" @@ -1509,6 +2177,16 @@ dependencies = [ "cc", ] +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1591,6 +2269,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1622,7 +2306,18 @@ dependencies = [ "byteorder-lite", "moxcms", "num-traits", - "png", + "png 0.18.1", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1632,17 +2327,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] -name = "inout" -version = "0.1.4" +name = "indoc" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ - "block-padding", - "generic-array", + "rustversion", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instability" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" +dependencies = [ + "darling 0.24.1", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -1672,6 +2400,99 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -1681,7 +2502,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.18", @@ -1702,6 +2523,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -1742,6 +2572,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr 0.7.1", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonptr" version = "0.8.1" @@ -1762,7 +2614,7 @@ dependencies = [ "bytecount", "data-encoding", "email_address", - "fancy-regex", + "fancy-regex 0.18.0", "fraction", "getrandom 0.3.4", "idna", @@ -1804,6 +2656,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + [[package]] name = "keyboard-types" version = "0.8.3" @@ -1814,6 +2677,12 @@ dependencies = [ "serde", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1880,6 +2749,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.25" @@ -1890,22 +2765,12 @@ dependencies = [ ] [[package]] -name = "libxdo" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" -dependencies = [ - "libxdo-sys", -] - -[[package]] -name = "libxdo-sys" -version = "0.11.0" +name = "line-clipping" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ - "libc", - "x11", + "bitflags 2.13.1", ] [[package]] @@ -1920,6 +2785,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1935,12 +2806,42 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac_address" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a" +dependencies = [ + "nix 0.30.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1962,6 +2863,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + [[package]] name = "memoffset" version = "0.9.1" @@ -1983,6 +2890,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2010,6 +2923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2045,13 +2959,13 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "libxdo", "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", + "serde", "thiserror 2.0.18", "windows-sys 0.61.2", ] @@ -2085,7 +2999,7 @@ version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92cec6048ac04041ac8055bdce1ec4f353416cd443ba9222bf73af790c3d4495" dependencies = [ - "convert_case", + "convert_case 0.12.0", "ctor", "napi-derive-backend", "proc-macro2", @@ -2099,7 +3013,7 @@ version = "6.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fae2444f4ba7333754bb73414ff8059dec2cb23b83a95bd1a0bf08169eed1be0" dependencies = [ - "convert_case", + "convert_case 0.12.0", "proc-macro2", "quote", "semver", @@ -2115,6 +3029,36 @@ dependencies = [ "libloading 0.9.0", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nemo-relay-plugin" version = "0.10.0" @@ -2143,12 +3087,53 @@ dependencies = [ "uuid", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2203,6 +3188,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2252,6 +3248,37 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc2" version = "0.6.4" @@ -2259,6 +3286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -2269,15 +3297,8 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", "objc2-quartz-core", ] @@ -2299,7 +3320,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -2339,28 +3359,25 @@ dependencies = [ ] [[package]] -name = "objc2-core-text" +name = "objc2-core-location" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "bitflags 2.13.1", "objc2", - "objc2-core-foundation", - "objc2-core-graphics", + "objc2-foundation", ] [[package]] -name = "objc2-core-video" +name = "objc2-core-text" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-io-surface", ] [[package]] @@ -2369,6 +3386,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -2377,7 +3403,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", "block2", - "libc", "objc2", "objc2-core-foundation", ] @@ -2401,6 +3426,52 @@ checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.1", "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", "objc2-foundation", ] @@ -2424,7 +3495,7 @@ checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags 2.13.1", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "openssl-macros", "openssl-sys", @@ -2559,12 +3630,54 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + [[package]] name = "pango" version = "0.18.3" @@ -2619,6 +3732,153 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +dependencies = [ + "pest", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[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_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.8", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2658,12 +3918,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" dependencies = [ "base64 0.23.1", - "indexmap", + "indexmap 2.14.0", "quick-xml", "serde", "time", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "png" version = "0.18.1" @@ -2683,6 +3956,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2707,6 +3989,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prefill-router" version = "0.3.0" @@ -3040,6 +4328,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.5" @@ -3071,6 +4368,12 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rand_core" version = "0.9.5" @@ -3096,84 +4399,191 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "ratatui" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ - "bitflags 2.13.1", + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", ] [[package]] -name = "redox_users" -version = "0.5.3" +name = "ratatui-core" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "libredox", + "bitflags 2.13.1", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", ] [[package]] -name = "ref-cast" -version = "1.0.26" +name = "ratatui-crossterm" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ - "ref-cast-impl", + "cfg-if", + "crossterm", + "instability", + "ratatui-core", ] [[package]] -name = "ref-cast-impl" -version = "1.0.26" +name = "ratatui-macros" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", + "ratatui-core", + "ratatui-widgets", ] [[package]] -name = "referencing" -version = "0.49.4" +name = "ratatui-termina" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf1a74e036be2546c0c81cdfad6b2def6a25bf31c4e34a468037058d3bd05c6" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" dependencies = [ - "ahash", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown", - "itoa", - "micromap", - "parking_lot", - "percent-encoding", - "serde_json", + "instability", + "ratatui-core", + "termina", ] [[package]] -name = "regex" -version = "1.13.1" +name = "ratatui-termwiz" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "ratatui-core", + "termwiz", ] [[package]] -name = "regex-automata" -version = "0.4.16" +name = "ratatui-widgets" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - + "bitflags 2.13.1", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "referencing" +version = "0.49.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf1a74e036be2546c0c81cdfad6b2def6a25bf31c4e34a468037058d3bd05c6" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.17.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.11" @@ -3221,6 +4631,29 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "percent-encoding", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.61.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -3307,7 +4740,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -3368,6 +4801,57 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3397,6 +4881,25 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "self-replace" version = "1.5.0" @@ -3413,6 +4916,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -3424,6 +4931,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3444,13 +4963,24 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "serde_json" version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -3469,6 +4999,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -3499,6 +5040,70 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3525,6 +5130,27 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3557,6 +5183,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + [[package]] name = "slab" version = "0.4.12" @@ -3590,20 +5222,98 @@ dependencies = [ ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" +name = "softbuffer" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.28.0" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ @@ -3628,6 +5338,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "switchyard-claude" version = "0.1.0" @@ -3658,21 +5379,34 @@ version = "0.3.0" dependencies = [ "chrono", "dbus-secret-service", + "dispatch2", "image", "libc", "mslnk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-quartz-core", "plist", + "ratatui", + "rfd", "security-framework", "self-replace", "serde", "serde_json", + "shlex", + "strum", "switchyard-costs", "switchyard-protocol", "switchyard-runner", + "tauri", + "tauri-build", "tempfile", "toml 0.9.12+spec-1.1.0", "toml_edit 0.25.13+spec-1.1.0", "url", + "uuid", + "windows", "windows-sys 0.61.2", ] @@ -3683,7 +5417,7 @@ dependencies = [ "async-trait", "futures", "http", - "jsonptr", + "jsonptr 0.8.1", "jsonschema", "opentelemetry", "opentelemetry_sdk", @@ -3729,23 +5463,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "switchyard-menubar" -version = "0.3.0" -dependencies = [ - "chrono", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "serde", - "serde_json", - "switchyard-costs", - "switchyard-protocol", - "tempfile", - "toml 0.9.12+spec-1.1.0", - "tray-icon", -] - [[package]] name = "switchyard-nemo-relay-plugin" version = "0.3.0" @@ -3941,6 +5658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -3999,6 +5717,57 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tao" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4012,86 +5781,386 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" +name = "tauri" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9" dependencies = [ - "thiserror-impl 1.0.69", + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", ] [[package]] -name = "thiserror" -version = "2.0.18" +name = "tauri-build" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45" dependencies = [ - "thiserror-impl 2.0.18", + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "tauri-codegen" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80" dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", "proc-macro2", "quote", + "semver", + "serde", + "serde_json", + "sha2", "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "tauri-macros" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.119", + "tauri-codegen", + "tauri-utils", ] [[package]] -name = "thread_local" -version = "1.1.10" +name = "tauri-runtime" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" dependencies = [ - "cfg-if", + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", ] [[package]] -name = "time" -version = "0.3.55" +name = "tauri-runtime-wry" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", ] [[package]] -name = "time-core" -version = "0.1.9" +name = "tauri-utils" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf 0.13.1", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.3+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.3+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.1", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bitflags 2.13.1", + "fancy-regex 0.11.0", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf 0.11.3", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" @@ -4209,7 +6278,7 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", @@ -4224,7 +6293,7 @@ version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", @@ -4266,7 +6335,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4277,7 +6346,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4290,7 +6359,7 @@ version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", @@ -4349,7 +6418,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -4484,7 +6553,8 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", + "serde", "thiserror 2.0.18", "windows-sys 0.61.2", ] @@ -4515,12 +6585,24 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -4539,6 +6621,23 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -4555,6 +6654,19 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", ] [[package]] @@ -4575,6 +6687,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ + "atomic", "getrandom 0.3.4", "js-sys", "serde", @@ -4621,6 +6734,35 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4743,6 +6885,62 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -4752,6 +6950,112 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4783,6 +7087,43 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4796,6 +7137,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4824,6 +7176,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4842,13 +7204,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4860,34 +7231,85 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4900,24 +7322,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4948,6 +7394,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "wiremock" version = "0.6.5" @@ -4983,6 +7439,50 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + [[package]] name = "x11" version = "2.21.0" @@ -4993,6 +7493,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 60228cabd..bd81b1cd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "crates/switchyard-nemo-relay-plugin", "crates/switchyard-runner", "crates/switchyard-desktop", - "crates/switchyard-menubar", "crates/switchyard-costs", "crates/switchyard-server", "crates/switchyard-skill-distillation", diff --git a/Makefile b/Makefile index 7136e3a26..eadc87cff 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 .DEFAULT_GOAL := help -.PHONY: help install-linux install-linux-dry-run uninstall-linux install-macos install-macos-dry-run uninstall-macos +.PHONY: help install-linux install-linux-dry-run uninstall-linux help: @echo "install-linux Install the systemd user service and Codex profile" @echo "install-linux-dry-run Preview installation without changes" @echo "uninstall-linux Remove the service and Codex profile" - @echo "install-macos Install the background server and menu bar app" - @echo "install-macos-dry-run Preview installation without changes" - @echo "uninstall-macos Remove the LaunchAgents and Codex profile" ## Install the Switchyard background server as a systemd user service. install-linux: @@ -23,15 +20,3 @@ install-linux-dry-run: ## Remove the systemd user service, the sy Codex profile, and the codex alias. uninstall-linux: @scripts/linux/uninstall.sh - -## Install the Switchyard background server and menu bar app for this user. -install-macos: - @scripts/macos/install.sh - -## Print what install-macos would do, without changing anything. -install-macos-dry-run: - @scripts/macos/install.sh --dry-run - -## Remove the LaunchAgents, the sy Codex profile, and the codex alias. -uninstall-macos: - @scripts/macos/uninstall.sh diff --git a/README.md b/README.md index 60ac10607..143076524 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,6 @@ Features and integrations recently merged into `main`. - **2026-10-12 · Bring Switchyard to TypeScript and Pi**\ Select models in Node.js or try automatic routing through Pi’s virtual models. [TypeScript bindings →](bindings/typescript/README.md) · [Pi example →](examples/pi/README.md) -- **2026-10-08 · Run Codex with Switchyard on macOS**\ - Install a background router and Codex profile, then track usage and estimated savings from the menu bar. [Setup guide →](crates/switchyard-menubar/README.md) - ## Community and license [Report an issue](https://github.com/NVIDIA-NeMo/Switchyard/issues) · [Contribute](CONTRIBUTING.md) · [Code of conduct](CODE_OF_CONDUCT.md) diff --git a/crates/switchyard-desktop/.gitignore b/crates/switchyard-desktop/.gitignore new file mode 100644 index 000000000..8d94b8818 --- /dev/null +++ b/crates/switchyard-desktop/.gitignore @@ -0,0 +1 @@ +/gen diff --git a/crates/switchyard-desktop/Cargo.toml b/crates/switchyard-desktop/Cargo.toml index 42d05821d..b59b0bc3a 100644 --- a/crates/switchyard-desktop/Cargo.toml +++ b/crates/switchyard-desktop/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "switchyard-desktop" version.workspace = true -description = "Installer and usage summary for a local Switchyard server" +description = "Installer and desktop app for a local Switchyard server" authors.workspace = true edition.workspace = true homepage = "https://github.com/NVIDIA-NeMo/Switchyard" @@ -12,6 +12,7 @@ license.workspace = true repository.workspace = true rust-version.workspace = true publish = false +default-run = "switchyard-desktop" [dependencies] switchyard-runner.workspace = true @@ -24,8 +25,17 @@ tempfile = "3" toml = "0.9" toml_edit = "0.25" url = "2" +uuid.workspace = true +ratatui = { version = "0.30.2", features = ["unstable-rendered-line-info"] } +shlex = "2" +strum = { version = "0.28", features = ["derive"] } [target.'cfg(target_os = "macos")'.dependencies] +objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["CALayer"] } +dispatch2 = "0.3" +objc2 = "0.6" +objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSWorkspace", "NSAccessibility", "NSApplication", "NSPanel", "NSColor", "NSWindow", "NSView", "NSScreen", "NSEvent", "NSStatusItem", "NSStatusBarButton", "NSButton", "NSControl", "NSResponder", "objc2-quartz-core"] } +objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSString", "NSURL", "NSProcessInfo"] } security-framework = "3" [target.'cfg(unix)'.dependencies] @@ -38,7 +48,15 @@ dbus-secret-service = { version = "4.1", features = ["crypto-rust", "vendored"] [target.'cfg(windows)'.dependencies] mslnk = "0.1.8" self-replace = "1.5" -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp", "Win32_Security_Credentials", "Win32_System_Console", "Win32_Storage_FileSystem"] } +windows = { version = "0.62.2", features = ["Management_Deployment", "ApplicationModel"] } +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp", "Win32_Security_Credentials", "Win32_System_Console", "Win32_Storage_FileSystem", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } + +[target.'cfg(any(target_os = "macos", windows))'.dependencies] +rfd = { version = "0.17", default-features = false } +tauri = { version = "2", features = ["tray-icon", "custom-protocol"] } + +[target.'cfg(any(target_os = "macos", windows))'.build-dependencies] +tauri-build = "2" [dev-dependencies] image = { version = "0.25", default-features = false, features = ["png"] } diff --git a/crates/switchyard-desktop/build.rs b/crates/switchyard-desktop/build.rs new file mode 100644 index 000000000..ce6e6a243 --- /dev/null +++ b/crates/switchyard-desktop/build.rs @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// Supported desktop platforms need Tauri resources and command metadata at build time. +fn main() { + #[cfg(any(target_os = "macos", windows))] + tauri_build::build(); +} diff --git a/crates/switchyard-desktop/src/gui.rs b/crates/switchyard-desktop/src/gui.rs new file mode 100644 index 000000000..ce5f445c3 --- /dev/null +++ b/crates/switchyard-desktop/src/gui.rs @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The desktop window shows server health and recorded request and token counts. + +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use switchyard_desktop::{config::Config, health::Probe, rollup}; +use tauri::State; + +/// The window retains the probe and reader so refreshes reuse the health worker and log offset. +struct Desktop { + settings: PathBuf, + health: Probe, + reader: rollup::Reader, +} + +#[derive(serde::Serialize)] +struct Counts { + requests: u64, + tokens: u64, +} + +#[derive(serde::Serialize)] +struct Snapshot { + server_url: String, + health: serde_json::Value, + today: Option, + week: Option, + errors: Vec, + refresh_seconds: u64, +} + +type Shared = Arc>; + +/// The worker reads health and usage while the mutex prevents overlapping refreshes. +#[tauri::command] +async fn snapshot(state: State<'_, Shared>) -> Result { + let shared = state.inner().clone(); + tauri::async_runtime::spawn_blocking(move || { + let mut desktop = shared + .try_lock() + .map_err(|_| "A refresh is running. Try again when it finishes.".to_string())?; + let config = Config::load(&desktop.settings)?; + let health = desktop.health.details(&config.server_url); + let mut errors = Vec::new(); + // A failed read leaves counts absent so the window cannot show incomplete usage as zero. + let usage = desktop + .reader + .read(&config.routing_log, chrono::Local::now().date_naive()) + .map_err(|error| errors.push(format!("Could not read usage: {error}"))) + .ok(); + let counts = |totals: &rollup::Totals| Counts { + requests: totals.requests(), + tokens: totals.tokens(), + }; + Ok(Snapshot { + server_url: config.server_url, + health, + today: usage.as_ref().map(|usage| counts(&usage.today)), + week: usage.as_ref().map(|usage| counts(&usage.week)), + errors, + refresh_seconds: config.refresh_seconds.max(1), + }) + }) + .await + .map_err(|error| error.to_string())? +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "snake_case")] +enum FileKind { + Config, + Logs, +} + +/// The worker waits for any refresh, then opens the configured file after releasing the mutex. +#[tauri::command] +async fn open_file(state: State<'_, Shared>, kind: FileKind) -> Result<(), String> { + let shared = state.inner().clone(); + tauri::async_runtime::spawn_blocking(move || { + let config = { + let desktop = shared + .lock() + .map_err(|_| "Could not access desktop state after a worker failed.".to_string())?; + Config::load(&desktop.settings)? + }; + let path = match kind { + FileKind::Config => config.config_file, + FileKind::Logs => config.routing_log, + }; + #[cfg(target_os = "macos")] + { + let output = std::process::Command::new("open") + .arg("--") + .arg(&path) + .output() + .map_err(|error| format!("Could not open {}: {error}", path.display()))?; + if !output.status.success() { + return Err(format!( + "Could not open {}: {}", + path.display(), + String::from_utf8_lossy(&output.stderr).trim() + )); + } + Ok(()) + } + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::UI::{ + Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOWNORMAL, + }; + let path: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + let status = unsafe { + ShellExecuteW( + std::ptr::null_mut(), + std::ptr::null(), + path.as_ptr(), + std::ptr::null(), + std::ptr::null(), + SW_SHOWNORMAL, + ) + } as isize; + if status > 32 { + Ok(()) + } else { + Err(format!("Windows could not open the file (error {status}).")) + } + } + }) + .await + .map_err(|error| error.to_string())? +} + +/// Quitting closes the window and stops the application. +#[tauri::command] +fn quit_app(app: tauri::AppHandle) { + app.exit(0); +} + +/// The application owns one health probe and log reader for the lifetime of the window. +pub fn run(settings: PathBuf) -> Result<(), String> { + tauri::Builder::default() + .manage(Arc::new(Mutex::new(Desktop { + settings, + health: Probe::default(), + reader: rollup::Reader::default(), + }))) + .invoke_handler(tauri::generate_handler![snapshot, open_file, quit_app]) + .run(tauri::generate_context!()) + .map_err(|error| error.to_string()) +} diff --git a/crates/switchyard-desktop/src/main.rs b/crates/switchyard-desktop/src/main.rs new file mode 100644 index 000000000..e1ba58f87 --- /dev/null +++ b/crates/switchyard-desktop/src/main.rs @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The desktop window shows local server health and recorded usage. + +#![cfg_attr(windows, windows_subsystem = "windows")] + +use switchyard_desktop::config; +#[cfg(any(target_os = "macos", windows))] +mod gui; + +use std::path::PathBuf; +use std::process::ExitCode; + +use config::Config; + +const USAGE: &str = "\ +Usage: switchyard-desktop [--validate-toml] [FILE] + +This app opens a window with server status and recorded usage. +The desktop window supports macOS and Windows. + + FILE The app reads settings from FILE. + --validate-toml Parse FILE as TOML and exit. + +Without FILE, the app uses bundled installation metadata to find desktop.toml. +Without that metadata, it uses $SY_HOME/desktop.toml, or ~/.switchyard/desktop.toml +when SY_HOME is unset. + +Missing settings use the directory and port from adjacent install.toml, or the +settings file's directory and port 4123 when that metadata is absent. +The default server URL includes the port, such as http://127.0.0.1:4123. +A configured HTTP URL without a port uses port 80."; + +/// The command checks TOML syntax or opens the desktop window. +fn main() -> ExitCode { + // A desktop launch may have no parent console, so attachment failure does not stop the app. + #[cfg(windows)] + let _ = unsafe { + windows_sys::Win32::System::Console::AttachConsole( + windows_sys::Win32::System::Console::ATTACH_PARENT_PROCESS, + ) + }; + let mut settings = None; + let mut validate_toml = false; + for arg in std::env::args().skip(1) { + match arg.as_str() { + "-h" | "--help" => { + println!("{USAGE}"); + return ExitCode::SUCCESS; + } + "--validate-toml" => validate_toml = true, + _ if arg.starts_with('-') => { + eprintln!("switchyard-desktop: unknown option {arg}\n\n{USAGE}"); + return ExitCode::FAILURE; + } + _ if settings.is_some() => { + eprintln!("switchyard-desktop: expected at most one settings file\n\n{USAGE}"); + return ExitCode::FAILURE; + } + _ => settings = Some(PathBuf::from(arg)), + } + } + + if validate_toml { + let Some(path) = settings else { + eprintln!("switchyard-desktop: --validate-toml requires a file path"); + return ExitCode::FAILURE; + }; + if let Err(error) = config::validate_toml_file(&path) { + eprintln!("switchyard-desktop: {error}"); + return ExitCode::FAILURE; + } + return ExitCode::SUCCESS; + } + + let settings = match settings.map(Ok).unwrap_or_else(Config::default_path) { + Ok(path) => path, + Err(error) => { + eprintln!("switchyard-desktop: {error}"); + return ExitCode::FAILURE; + } + }; + #[cfg(any(target_os = "macos", windows))] + match gui::run(settings) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("switchyard-desktop: {error}"); + ExitCode::FAILURE + } + } + #[cfg(not(any(target_os = "macos", windows)))] + { + let _ = settings; + eprintln!("The desktop window supports macOS and Windows."); + ExitCode::FAILURE + } +} diff --git a/crates/switchyard-desktop/tauri.conf.json b/crates/switchyard-desktop/tauri.conf.json new file mode 100644 index 000000000..3ca080f16 --- /dev/null +++ b/crates/switchyard-desktop/tauri.conf.json @@ -0,0 +1,32 @@ +{ + "productName": "Switchyard", + "version": "0.3.0", + "identifier": "com.nvidia.switchyard", + "build": { + "frontendDist": "ui" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "Switchyard", + "width": 680, + "height": 500, + "minWidth": 440, + "minHeight": 400 + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self'; connect-src ipc: http://ipc.localhost; img-src 'self' data:; object-src 'none'; frame-src 'none'; base-uri 'none'" + } + }, + "bundle": { + "active": false, + "icon": [ + "icons/icon.icns", + "icons/icon.png", + "icons/icon.ico" + ] + } +} diff --git a/crates/switchyard-desktop/ui/basic.css b/crates/switchyard-desktop/ui/basic.css new file mode 100644 index 000000000..d8e88ae37 --- /dev/null +++ b/crates/switchyard-desktop/ui/basic.css @@ -0,0 +1,11 @@ +/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */ +/* SPDX-License-Identifier: Apache-2.0 */ +:root { color-scheme: light dark; font-family: system-ui, sans-serif; } +body { margin: 0; background: Canvas; color: CanvasText; } +main { max-width: 42rem; padding: 2rem; margin: auto; } +h1 { margin-top: 0; } +section { margin-block: 2rem; } +.actions { display: flex; flex-wrap: wrap; gap: .6rem; } +button { font: inherit; padding: .5rem .8rem; cursor: pointer; } +#server-url, #message { overflow-wrap: anywhere; } +#message { white-space: pre-wrap; } diff --git a/crates/switchyard-desktop/ui/basic.js b/crates/switchyard-desktop/ui/basic.js new file mode 100644 index 000000000..96c249532 --- /dev/null +++ b/crates/switchyard-desktop/ui/basic.js @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const invoke = window.__TAURI__.core.invoke; +const message = document.getElementById('message'); +let timer; +let busy = false; + +/** Missing usage stays unavailable so a failed read cannot appear as zero. */ +function counts(label, value) { + return value + ? `${label}: ${value.requests.toLocaleString()} requests · ${value.tokens.toLocaleString()} tokens` + : `${label}: usage unavailable`; +} + +/** Each refresh updates the window and schedules the next check, including after errors. */ +async function refresh() { + if (busy) return; + busy = true; + clearTimeout(timer); + let seconds = 30; + try { + const view = await invoke('snapshot'); + document.getElementById('server').textContent = view.health.state === 'running' + ? 'Server is running' : `Server is not responding: ${view.health.reason}`; + document.getElementById('server-url').textContent = view.server_url; + document.getElementById('today').textContent = counts('Today', view.today); + document.getElementById('week').textContent = counts('Last 7 days', view.week); + message.textContent = view.errors.join('\n'); + seconds = view.refresh_seconds; + } catch (error) { + document.getElementById('server').textContent = 'Server status unavailable'; + document.getElementById('server-url').textContent = ''; + document.getElementById('today').textContent = counts('Today', null); + document.getElementById('week').textContent = counts('Last 7 days', null); + message.textContent = String(error); + } finally { + busy = false; + timer = setTimeout(refresh, Math.min(seconds * 1000, 2147483647)); + } +} + +document.getElementById('refresh').addEventListener('click', refresh); +for (const [id, kind] of [['open-config', 'config'], ['open-logs', 'logs']]) { + document.getElementById(id).addEventListener('click', async () => { + try { + await invoke('open_file', { kind }); + message.textContent = ''; + } catch (error) { + message.textContent = String(error); + } + }); +} +document.getElementById('quit').addEventListener('click', () => invoke('quit_app')); +refresh(); diff --git a/crates/switchyard-desktop/ui/index.html b/crates/switchyard-desktop/ui/index.html new file mode 100644 index 000000000..589aa4769 --- /dev/null +++ b/crates/switchyard-desktop/ui/index.html @@ -0,0 +1,32 @@ + + + + + + + + Switchyard + + + + +
+

Switchyard

+

Checking the server…

+

+
+

Recorded usage

+

Today: loading…

+

Last 7 days: loading…

+

Requests count answers. Tokens include classifier calls.

+
+
+ + + + +
+

+
+ + diff --git a/crates/switchyard-menubar/Cargo.toml b/crates/switchyard-menubar/Cargo.toml deleted file mode 100644 index b839ad9c9..000000000 --- a/crates/switchyard-menubar/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -[package] -name = "switchyard-menubar" -version.workspace = true -description = "macOS menu bar companion for a locally running Switchyard server" -authors.workspace = true -edition.workspace = true -homepage = "https://github.com/NVIDIA-NeMo/Switchyard" -license.workspace = true -repository.workspace = true -rust-version.workspace = true -publish = false - -[dependencies] -switchyard-costs.workspace = true -switchyard-protocol.workspace = true -chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } -serde.workspace = true -serde_json.workspace = true -toml = "0.9" - -[target.'cfg(target_os = "macos")'.dependencies] -objc2 = "0.6" -objc2-app-kit = "0.3" -objc2-foundation = "0.3" -tray-icon = "0.25" - -[dev-dependencies] -tempfile = "3" diff --git a/crates/switchyard-menubar/README.md b/crates/switchyard-menubar/README.md deleted file mode 100644 index c58c9d0b3..000000000 --- a/crates/switchyard-menubar/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# switchyard-menubar - -A macOS menu bar companion for a Switchyard server running in the background. - -The server does the routing. This process only reads what the server already -wrote, so quitting it never affects traffic. - -Click the glyph to see whether the server is answering, today's and this -week's requests and tokens, estimated savings, and which models served the -week. It can also restart the server and open either config file. - -## Install - -From the repository root: - -```sh -make install-macos # or install-macos-dry-run to see the steps first -make uninstall-macos -``` - -That builds `switchyard-server` and `switchyard-menubar` into -`~/.switchyard/bin`, writes a server config and menu bar settings, loads two -LaunchAgents, and writes a `sy` Codex profile. Use the CLI profile with -`codex -p sy`. - -Your server config and menu bar settings are never overwritten once they -exist, so edits survive a reinstall. Logs are in `~/.switchyard/logs/`. - -## Settings - -`~/.switchyard/menubar.toml`. Any key may be omitted. - -```toml -server_url = "http://127.0.0.1:4123" -routing_log = "~/.switchyard/routing.jsonl" -baseline_model = "gpt-5.6-sol" -refresh_seconds = 30 - -# This optional file replaces the bundled price table. -# pricing_file = "~/.switchyard/prices.toml" -``` - -The server and menubar use the same [bundled rates](../switchyard-costs/prices.toml) by -default. To override them, point the server's `--pricing-file` and the menubar's -`pricing_file` at the same TOML or JSON file. The file replaces the entire bundled table. -Bundled rates stay fixed until you install a version with updated rates. The table -includes text-token rates for OpenAI, Claude, Gemini, and DeepSeek models, plus OpenRouter -rates for other popular models. Direct-provider and OpenRouter rates use separate model -IDs, such as `gemini-3.8-flash` and `google/gemini-3.8-flash`. Estimates exclude -long-context and service-tier adjustments, OpenAI's 12-hour cache-write pricing, tool -fees, and time-based cache storage. Each model has one rate at all times of day. The -provider that OpenRouter selects can change the charge. Some Gemini rates are promotions; -the table lists their end dates. Use an override when your provider charges different rates. -Key each entry by the model ID in the routing log. Rates are in USD per million tokens: - -```toml -["gpt-5.6-sol"] -input = 4 -cache_read = 0.4 -cache_write = 5 -output = 20 -``` - -Use a `.toml` extension for TOML. The extension check ignores case; other filenames -are read as JSON. - -These example rates are estimates. Replace them with your provider's rates. -The [pi guide](../../docs/integrations/pi.md#estimate-the-cost) explains -cache-write rates and how to add other models. Rates load at startup; restart both -applications after editing them. An unreadable or invalid override file stops startup; -the applications do not use bundled rates instead. - -## How savings are computed - -Actual spending is the sum of the costs saved in the routing log, including routing -calls. Changing prices does not change saved costs. The baseline is an estimate of -what the answer traffic would have cost using `baseline_model`; it excludes routing -calls. The server and menubar share the code for cost totals and token pricing, including -cache-read and cache-write rates. -The baseline assumes the same token and cache counts as the observed answer traffic. - -The menubar shows estimated savings only when actual costs and the baseline estimate -are complete. A recorded `cost: null` keeps spending unknown. A missing baseline rate -or required one-hour cache-write rate keeps the baseline unknown. Missing or invalid answer -usage also keeps the baseline unknown. Token counts still -appear when savings cannot be calculated. The log can be read while the server is offline. - -The bundled `codex-auto-review` rates match `gpt-6-sol`. Codex sends these approval -reviews even without Switchyard. If your `baseline_model` uses different rates, add -matching `codex-auto-review` rates to your override file to give those reviews the -same estimated cost with and without Switchyard. Matching review costs leave dollar -savings unchanged but reduce the savings percentage. ChatGPT subscription traffic -may have no per-token bill; token-rate estimates are comparisons at the supplied rates. - -## Migrate older settings and logs - -Inline `[prices]` settings and log records without a `cost` field require migration. -Stop the server and menubar before migrating. From the repository root, run: - -```sh -cargo run -p switchyard-costs -- \ - ~/.switchyard/menubar.toml ~/.switchyard/routing.jsonl ~/.switchyard/migrated-costs -``` - -The destination directory must not exist. The command writes `menubar.toml`, -`prices.json`, and `routing.jsonl` there and leaves the originals untouched. It preserves -existing costs, including null, and adds estimates only to records without a cost field. -Missing usage or rates leave the reconstructed cost unknown. Malformed or unfinished -records stop migration; failed migration outputs are removed. - -The command uses the rates in the old settings. It cannot recover historical provider -charges or historical prices. It maps `input_per_mtok` to `input` and `cache_write`, -`output_per_mtok` to `output`, and `cached_input_per_mtok` to `cache_read` (or the input -rate when omitted). This preserves the old cache-write assumption. Review those rates -for your providers; add `cache_write_1h` when needed. Reconstruction is always marked -`estimated`, even when it uses historical prices. - -Review the generated files, then start the menubar with the generated settings path. -Those settings point at the generated log and pricing file. Update the server's -LaunchAgent to use those same paths for `--routing-log-file` and `--pricing-file` before -restarting it. Keep the originals as backups. Reinstalling preserves existing files; -it does not migrate them. - -## Codex profiles - -`codex --profile sy` reads `~/.codex/sy.config.toml`. The installer writes this -standalone profile and keeps your existing `~/.codex/config.toml` unchanged. -Reinstalling backs up the profile before replacing it if its contents change. - -The profile sets `model`, `model_provider`, and the Switchyard provider details. -It leaves `approval_policy` and `sandbox_mode` to your existing Codex settings. - -Codex.app cannot use the profile. The installer leaves its config unchanged. -For older installs that replaced `config.toml`, `make uninstall-macos` restores -`config.toml.direct` and keeps a copy of the active config before replacing it. diff --git a/crates/switchyard-menubar/src/app.rs b/crates/switchyard-menubar/src/app.rs deleted file mode 100644 index 3695ce9b0..000000000 --- a/crates/switchyard-menubar/src/app.rs +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Ties the pieces together: read the log, probe the server, build the rows. - -use chrono::Local; - -use crate::config::Config; -use crate::health::probe; -use crate::rollup; -use crate::summary::{Row, build}; - -/// Reads new log entries, probes the server, and builds the menu body. -pub fn refresh(config: &Config, log: &mut rollup::Reader) -> Vec { - let usage = match log.read(&config.routing_log, Local::now().date_naive()) { - Ok(usage) => usage, - Err(error) => return vec![Row::Label(format!("Cannot read routing log: {error}"))], - }; - build(probe(&config.server_url), &usage, config) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reads_the_configured_log() { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("routing.jsonl"); - let ts = Local::now().to_rfc3339(); - std::fs::write( - &path, - format!( - r#"{{"ts":"{ts}","model":"luna","tier":"","prompt_tokens":1000,"cached_tokens":0,"completion_tokens":100,"reasoning_tokens":0,"cost":null}}"# - ) + "\n", - ) - .expect("write log"); - - let rows = refresh( - &Config { - routing_log: path, - ..Config::default() - }, - &mut rollup::Reader::default(), - ); - - assert!( - rows.contains(&Row::Label("Today — 1 request · 1100 tokens".to_string())), - "unexpected rows: {rows:?}" - ); - } -} diff --git a/crates/switchyard-menubar/src/config.rs b/crates/switchyard-menubar/src/config.rs deleted file mode 100644 index 185fca071..000000000 --- a/crates/switchyard-menubar/src/config.rs +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Menu bar settings, read from `~/.switchyard/menubar.toml`. - -use std::path::{Path, PathBuf}; - -use serde::Deserialize; - -use switchyard_costs::Pricing; - -/// Everything the menu bar needs to find the server and price its traffic. -/// Missing keys fall back to [`Config::default`]. -#[derive(Clone, Debug, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct Config { - /// Base URL the server listens on, used for the health check. - pub server_url: String, - /// JSONL file the server appends routing records to. - pub routing_log: PathBuf, - /// Server TOML, opened by the "Open server config" menu item. - pub config_file: PathBuf, - /// LaunchAgent label used to restart the server. - pub launchd_label: String, - /// How often the menu contents are recomputed. - pub refresh_seconds: u64, - /// Model the traffic is assumed to have used without Switchyard. - pub baseline_model: String, - /// An optional TOML or JSON file replaces bundled rates for the baseline estimate. - pub pricing_file: Option, - /// Rates load once when the settings are read. - #[serde(skip)] - pub prices: Pricing, -} - -impl Default for Config { - fn default() -> Self { - Self { - server_url: "http://127.0.0.1:4123".to_string(), - routing_log: PathBuf::from("~/.switchyard/routing.jsonl"), - config_file: PathBuf::from("~/.switchyard/composite.toml"), - launchd_label: "com.nvidia.switchyard.server".to_string(), - refresh_seconds: 30, - baseline_model: "gpt-5.6-sol".to_string(), - pricing_file: None, - prices: Pricing::default(), - } - } -} - -impl Config { - /// Parses settings, falling back to defaults when the file is absent. - pub fn load(path: &Path) -> Result { - let mut config: Self = match std::fs::read_to_string(path) { - Ok(text) => toml::from_str(&text) - .map_err(|error| format!("parse {}: {error}", path.display()))?, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Self::default(), - Err(error) => return Err(format!("read {}: {error}", path.display())), - }; - if let Some(path) = config.pricing_file.as_ref() { - let path = expand_home(path); - config.prices = Pricing::load(&path)?; - config.pricing_file = Some(path); - } else { - config.prices = Pricing::bundled()?; - } - config.routing_log = expand_home(&config.routing_log); - config.config_file = expand_home(&config.config_file); - Ok(config) - } - - /// Default settings path, `~/.switchyard/menubar.toml`. - pub fn default_path() -> PathBuf { - expand_home(Path::new("~/.switchyard/menubar.toml")) - } -} - -pub fn validate_toml_file(path: &Path) -> Result<(), String> { - let text = std::fs::read_to_string(path) - .map_err(|error| format!("read {}: {error}", path.display()))?; - toml::from_str::(&text) - .map_err(|error| format!("parse {}: {error}", path.display()))?; - Ok(()) -} - -/// Rewrites a leading `~`. Paths are written by hand in the settings file, -/// where `~` is the natural way to spell a home path. -pub fn expand_home(path: &Path) -> PathBuf { - let home = std::env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); - expand_under(path, &home) -} - -fn expand_under(path: &Path, home: &Path) -> PathBuf { - match path.strip_prefix("~") { - Ok(rest) => home.join(rest), - Err(_) => path.to_path_buf(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reads_settings_and_prices() { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("menubar.toml"); - std::fs::write( - &path, - r#" -server_url = "http://127.0.0.1:9000" -baseline_model = "sol" - -"#, - ) - .expect("write settings"); - - let config = Config::load(&path).expect("load"); - - assert_eq!(config.server_url, "http://127.0.0.1:9000"); - assert_eq!(config.baseline_model, "sol"); - assert!(config.pricing_file.is_none()); - assert_eq!(config.refresh_seconds, 30, "unset keys keep their default"); - } - - #[test] - fn a_missing_file_yields_defaults() { - let dir = tempfile::tempdir().expect("tempdir"); - - let config = Config::load(&dir.path().join("absent.toml")).expect("load"); - - assert_eq!(config.server_url, "http://127.0.0.1:4123"); - assert!(config.pricing_file.is_none()); - let empty = dir.path().join("empty.toml"); - std::fs::write(&empty, "").expect("write settings"); - let loaded = Config::load(&empty).expect("load empty settings"); - assert_eq!(config.routing_log, loaded.routing_log); - assert_eq!(config.config_file, loaded.config_file); - } - - #[test] - fn rejects_unparsable_settings() { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("menubar.toml"); - std::fs::write(&path, "server_url = ").expect("write settings"); - - assert!(Config::load(&path).is_err()); - } - - #[test] - fn validates_arbitrary_toml_files() { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("codex.toml"); - std::fs::write(&path, "[model_providers.\"sy\"]\nname = \"Switchyard\"\n") - .expect("write config"); - - assert!(validate_toml_file(&path).is_ok()); - - std::fs::write(&path, "[model_providers.\"sy\"]\nname = ").expect("write config"); - assert!(validate_toml_file(&path).is_err()); - } - - #[test] - fn expands_a_leading_tilde() { - let home = Path::new("/Users/example"); - - assert_eq!( - expand_under(Path::new("~/.switchyard/routing.jsonl"), home), - PathBuf::from("/Users/example/.switchyard/routing.jsonl") - ); - assert_eq!( - expand_under(Path::new("/tmp/routing.jsonl"), home), - PathBuf::from("/tmp/routing.jsonl") - ); - } -} diff --git a/crates/switchyard-menubar/src/health.rs b/crates/switchyard-menubar/src/health.rs deleted file mode 100644 index 9105ca75a..000000000 --- a/crates/switchyard-menubar/src/health.rs +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Liveness probe against the local server's `/health` endpoint. -//! -//! The request is written by hand over TCP. The server is always on loopback -//! over plain HTTP, so a full HTTP client would buy nothing here. - -use std::io::{BufRead, BufReader, Write}; -use std::net::{TcpStream, ToSocketAddrs}; -use std::time::Duration; - -/// How long the probe waits before calling the server unreachable. Short, -/// because it runs on the thread that draws the menu. -const TIMEOUT: Duration = Duration::from_millis(500); - -/// Whether the server answered its health check. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ServerStatus { - Running, - Stopped, -} - -/// Probes `GET /health`, treating any failure as a stopped server. -pub fn probe(server_url: &str) -> ServerStatus { - match request(server_url) { - Ok(true) => ServerStatus::Running, - _ => ServerStatus::Stopped, - } -} - -fn request(server_url: &str) -> std::io::Result { - let authority = authority(server_url); - let address = authority - .to_socket_addrs()? - .next() - .ok_or_else(|| std::io::Error::other(format!("no address for {authority}")))?; - let mut stream = TcpStream::connect_timeout(&address, TIMEOUT)?; - stream.set_read_timeout(Some(TIMEOUT))?; - stream.set_write_timeout(Some(TIMEOUT))?; - stream.write_all(b"GET /health HTTP/1.0\r\n\r\n")?; - let mut status_line = String::new(); - BufReader::new(stream).read_line(&mut status_line)?; - Ok(status_line.contains(" 200")) -} - -fn authority(server_url: &str) -> String { - let rest = server_url.rsplit("://").next().unwrap_or(server_url); - let mut authority = rest.split('/').next().unwrap_or(rest).to_string(); - let host_end = authority.rfind(']').map_or(0, |index| index + 1); - if !authority[host_end..].contains(':') { - authority.push_str(":4123"); - } - - authority -} - -#[cfg(test)] -mod tests { - use super::*; - use std::net::TcpListener; - - /// Serves one canned response, then closes. - fn serve(response: &'static str) -> String { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); - let port = listener.local_addr().expect("addr").port(); - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let _ = stream.write_all(response.as_bytes()); - } - }); - format!("http://127.0.0.1:{port}") - } - - #[test] - fn reports_running_only_on_a_200() { - let ok = serve("HTTP/1.1 200 OK\r\n\r\n"); - let down = serve("HTTP/1.1 503 Service Unavailable\r\n\r\n"); - - assert_eq!(probe(&ok), ServerStatus::Running); - assert_eq!(probe(&down), ServerStatus::Stopped); - } - - #[test] - fn reports_stopped_when_nothing_is_listening() { - // Binding then dropping yields a port with no listener. - let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); - let port = listener.local_addr().expect("addr").port(); - drop(listener); - - assert_eq!( - probe(&format!("http://127.0.0.1:{port}")), - ServerStatus::Stopped - ); - } - - #[test] - fn defaults_the_port_after_an_ipv6_literal() { - assert_eq!(authority("http://[::1]/v1"), "[::1]:4123"); - assert_eq!(authority("http://[::1]:9000/v1"), "[::1]:9000"); - assert_eq!(authority("http://localhost"), "localhost:4123"); - } - - #[test] - fn accepts_a_url_with_a_path_or_no_port() { - let ok = serve("HTTP/1.1 200 OK\r\n\r\n"); - - assert_eq!(probe(&format!("{ok}/v1")), ServerStatus::Running); - assert_eq!(probe("http://localhost"), ServerStatus::Stopped); - } -} diff --git a/crates/switchyard-menubar/src/icon.rs b/crates/switchyard-menubar/src/icon.rs deleted file mode 100644 index 5139e4ad0..000000000 --- a/crates/switchyard-menubar/src/icon.rs +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! The menu bar glyph: a track that splits in two, drawn in code. -//! -//! macOS scales the image to 18 points tall, so it is drawn at 2x for retina -//! displays. It is a template image, so only the alpha channel matters and the -//! system recolors it for light and dark menu bars. - -/// Side length of the generated image, in pixels. -pub const SIZE: u32 = 36; - -/// Half the stroke width, in pixels. -const HALF_STROKE: f64 = 1.7; - -const SEGMENTS: [((f64, f64), (f64, f64)); 3] = [ - ((5.0, 18.0), (17.0, 18.0)), - ((17.0, 18.0), (30.0, 7.0)), - ((17.0, 18.0), (30.0, 29.0)), -]; - -/// Renders the glyph as RGBA pixels, row-major from the top left. -pub fn glyph() -> Vec { - let mut pixels = Vec::with_capacity((SIZE * SIZE * 4) as usize); - for y in 0..SIZE { - for x in 0..SIZE { - let point = (f64::from(x) + 0.5, f64::from(y) + 0.5); - let distance = SEGMENTS - .iter() - .map(|(start, end)| distance_to_segment(point, *start, *end)) - .fold(f64::INFINITY, f64::min); - // One pixel of falloff past the stroke edge softens the diagonals. - let coverage = (HALF_STROKE + 0.5 - distance).clamp(0.0, 1.0); - pixels.extend_from_slice(&[0, 0, 0, (coverage * 255.0).round() as u8]); - } - } - pixels -} - -fn distance_to_segment(point: (f64, f64), start: (f64, f64), end: (f64, f64)) -> f64 { - let (dx, dy) = (end.0 - start.0, end.1 - start.1); - let length_squared = dx * dx + dy * dy; - let along = ((point.0 - start.0) * dx + (point.1 - start.1) * dy) / length_squared; - let along = along.clamp(0.0, 1.0); - let (cx, cy) = (start.0 + dx * along, start.1 + dy * along); - ((point.0 - cx).powi(2) + (point.1 - cy).powi(2)).sqrt() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn alpha(pixels: &[u8], x: u32, y: u32) -> u8 { - pixels[((y * SIZE + x) * 4 + 3) as usize] - } - - #[test] - fn draws_a_trunk_that_splits_in_two() { - let pixels = glyph(); - - assert_eq!(pixels.len(), (SIZE * SIZE * 4) as usize); - assert_eq!(alpha(&pixels, 10, 18), 255, "the trunk is solid"); - assert!(alpha(&pixels, 29, 7) > 0, "upper branch reaches its end"); - assert!(alpha(&pixels, 29, 29) > 0, "lower branch reaches its end"); - assert_eq!(alpha(&pixels, 29, 18), 0, "nothing runs straight through"); - assert_eq!(alpha(&pixels, 0, 0), 0, "corners stay transparent"); - } -} diff --git a/crates/switchyard-menubar/src/main.rs b/crates/switchyard-menubar/src/main.rs deleted file mode 100644 index fd42bc616..000000000 --- a/crates/switchyard-menubar/src/main.rs +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Menu bar companion for a locally running Switchyard server. -//! -//! The server owns routing; this process only reads what the server already -//! wrote. It shows today's and this week's traffic, and what that traffic -//! would have cost had every call gone to the capable model instead. - -mod app; -mod config; -mod health; -mod pricing; -mod rollup; -mod summary; -// The status item and the glyph it draws are the only platform-specific code. -#[cfg(target_os = "macos")] -mod icon; -#[cfg(target_os = "macos")] -mod tray; - -use std::path::PathBuf; -use std::process::ExitCode; - -use config::Config; - -const USAGE: &str = "\ -Usage: switchyard-menubar [--print | --validate-toml] [FILE] - -Shows Switchyard usage and estimated savings in the macOS menu bar. - - FILE Settings file [default: ~/.switchyard/menubar.toml] - --print Print the current summary and exit, instead of running - --validate-toml Parse FILE as TOML and exit"; - -fn main() -> ExitCode { - let mut settings = None; - let mut print_only = false; - let mut validate_toml = false; - for arg in std::env::args().skip(1) { - match arg.as_str() { - "-h" | "--help" => { - println!("{USAGE}"); - return ExitCode::SUCCESS; - } - "--print" => print_only = true, - "--validate-toml" => validate_toml = true, - _ if arg.starts_with('-') => { - eprintln!("switchyard-menubar: unknown option {arg}\n\n{USAGE}"); - return ExitCode::FAILURE; - } - _ => settings = Some(PathBuf::from(arg)), - } - } - - if validate_toml { - let Some(path) = settings else { - eprintln!("switchyard-menubar: --validate-toml requires a file path"); - return ExitCode::FAILURE; - }; - if let Err(error) = config::validate_toml_file(&path) { - eprintln!("switchyard-menubar: {error}"); - return ExitCode::FAILURE; - } - return ExitCode::SUCCESS; - } - - let settings_path = settings.unwrap_or_else(Config::default_path); - let config = match Config::load(&settings_path) { - Ok(config) => config, - Err(error) => { - eprintln!("switchyard-menubar: {error}"); - return ExitCode::FAILURE; - } - }; - // Off macOS there is no menu bar to attach to, so printing is the whole - // program. That keeps the rollup and pricing logic buildable everywhere. - if print_only || !cfg!(target_os = "macos") { - for row in app::refresh(&config, &mut rollup::Reader::default()) { - match row { - summary::Row::Separator => println!(), - summary::Row::Label(text) => println!("{text}"), - } - } - return ExitCode::SUCCESS; - } - - #[cfg(target_os = "macos")] - if let Err(error) = tray::run(config, settings_path) { - eprintln!("switchyard-menubar: {error}"); - return ExitCode::FAILURE; - } - ExitCode::SUCCESS -} diff --git a/crates/switchyard-menubar/src/pricing.rs b/crates/switchyard-menubar/src/pricing.rs deleted file mode 100644 index 5cff7ac49..000000000 --- a/crates/switchyard-menubar/src/pricing.rs +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Counterfactual cost: what routed traffic cost against what it would have -//! cost had every call gone to one capable model. - -use switchyard_costs::{CostTotals, Pricing}; -use switchyard_protocol::{InputCacheUsage, Usage}; - -use crate::rollup::Totals; - -/// What the traffic cost, and what it would have cost without routing. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Savings { - /// Cost of the calls that actually ran, classifier overhead included. - pub actual: f64, - /// Cost of the same caller-facing calls had they all used the baseline model. - pub baseline: f64, -} - -impl Savings { - /// Dollars kept. Negative when routing overhead outweighed the cheaper tier. - pub fn saved(&self) -> f64 { - self.baseline - self.actual - } - - /// Fraction of the baseline bill avoided, or `None` with nothing to compare. - pub fn percent(&self) -> Option { - (self.baseline > 0.0).then(|| self.saved() / self.baseline * 100.0) - } -} - -/// Prices a period against the baseline model. -/// -/// Returns `None` when recorded spending or the baseline estimate is incomplete. -pub fn estimate(totals: &Totals, prices: &Pricing, baseline_model: &str) -> Option { - if totals.baseline_unknown { - return None; - } - let mut baseline = CostTotals::default(); - for tokens in totals.routed.values() { - let usage = Usage { - input_tokens: Some(tokens.input), - output_tokens: Some(tokens.output), - cache: Some(Box::new(InputCacheUsage { - cached_input_tokens: Some(tokens.cached_input), - cache_creation_input_tokens: Some(tokens.cache_write), - cache_creation_1h_input_tokens: Some(tokens.cache_write_1h), - })), - ..Usage::default() - }; - baseline.add(prices.estimate(baseline_model, &usage)); - } - Some(Savings { - actual: totals.cost.total_usd?, - baseline: baseline.total_usd?, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::rollup::ModelTokens; - - fn table() -> Pricing { - Pricing::parse( - br#"{ - "sol":{"input":1.25,"cache_read":1.25,"cache_write":1.25,"output":10}, - "luna":{"input":0.25,"cache_read":0.25,"cache_write":0.25,"output":2}, - "terra":{"input":0.05,"cache_read":0.05,"cache_write":0.05,"output":0.4} - }"#, - ) - .expect("rates") - } - - fn record_cost(totals: &mut Totals, usd: f64) { - totals.cost.add(Some(switchyard_costs::RecordedCost { - usd, - source: switchyard_costs::CostSource::Estimated, - })); - } - - fn tokens(input: u64, output: u64) -> ModelTokens { - ModelTokens { - requests: 1, - input, - cached_input: 0, - output, - ..ModelTokens::default() - } - } - - #[test] - fn prices_the_cheaper_tier_against_the_capable_baseline() { - let mut totals = Totals::default(); - totals - .routed - .insert("luna".to_string(), tokens(1_000_000, 100_000)); - - record_cost(&mut totals, 0.45); - let savings = estimate(&totals, &table(), "sol").expect("priced"); - - assert!((savings.actual - 0.45).abs() < 1e-9); - assert!((savings.baseline - 2.25).abs() < 1e-9); - assert!((savings.percent().expect("percent") - 80.0).abs() < 1e-9); - } - - #[test] - fn settings_load_bundled_groups_for_baseline_estimates() -> Result<(), String> { - let dir = tempfile::tempdir().map_err(|error| error.to_string())?; - let config = crate::config::Config::load(&dir.path().join("menubar.toml"))?; - let mut totals = Totals::default(); - totals.routed.insert( - "answer-model".to_string(), - ModelTokens { - requests: 1, - input: 1_000_000, - output: 1_000_000, - cached_input: 1_000_000, - cache_write: 2_000_000, - cache_write_1h: 1_000_000, - }, - ); - record_cost(&mut totals, 0.5); - for model in ["claude-haiku-4-5", "anthropic/claude-haiku-4.5"] { - let savings = estimate(&totals, &config.prices, model).ok_or("missing baseline")?; - assert!((savings.baseline - 9.35).abs() < 1e-9, "{model}"); - assert!((savings.saved() - 8.85).abs() < 1e-9, "{model}"); - } - Ok(()) - } - - #[test] - fn classifier_overhead_counts_against_savings() { - let mut totals = Totals::default(); - totals - .routed - .insert("luna".to_string(), tokens(1_000_000, 100_000)); - record_cost(&mut totals, 0.45); - let before = estimate(&totals, &table(), "sol").expect("priced"); - - totals - .classifier - .insert("terra".to_string(), tokens(1_000_000, 10_000)); - record_cost(&mut totals, 0.054); - let after = estimate(&totals, &table(), "sol").expect("priced"); - - assert_eq!(after.baseline, before.baseline); - assert!(after.saved() < before.saved()); - } - - #[test] - fn cache_reads_use_the_cached_rate() { - let prices = - Pricing::parse(br#"{"sol":{"input":1,"cache_read":0.1,"cache_write":1,"output":0}}"#) - .expect("rates"); - let mut totals = Totals::default(); - totals.routed.insert( - "sol".to_string(), - ModelTokens { - requests: 1, - input: 1_000_000, - cached_input: 1_000_000, - output: 0, - ..ModelTokens::default() - }, - ); - - record_cost(&mut totals, 1.1); - let savings = estimate(&totals, &prices, "sol").expect("priced"); - - assert!((savings.actual - 1.1).abs() < 1e-9); - } - - #[test] - fn an_unknown_cost_suppresses_savings() { - let mut totals = Totals::default(); - totals - .routed - .insert("unknown".to_string(), tokens(1_000, 100)); - - totals.cost.add(None); - assert!(estimate(&totals, &table(), "sol").is_none()); - assert!(estimate(&totals, &table(), "absent").is_none()); - } - - #[test] - fn an_empty_period_has_no_percentage() { - let savings = estimate(&Totals::default(), &table(), "sol").expect("priced"); - - assert_eq!(savings.saved(), 0.0); - assert!(savings.percent().is_none()); - } -} diff --git a/crates/switchyard-menubar/src/rollup.rs b/crates/switchyard-menubar/src/rollup.rs deleted file mode 100644 index 0641ccb73..000000000 --- a/crates/switchyard-menubar/src/rollup.rs +++ /dev/null @@ -1,494 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Token totals for today and the past week, read from the server's routing log. - -use std::collections::BTreeMap; -use std::fs::{File, Metadata}; -use std::io::{BufRead, BufReader, Seek, SeekFrom}; -use std::path::Path; - -use chrono::{DateTime, Local, NaiveDate}; -use serde::Deserialize; -use switchyard_costs::{CostTotals, RecordedCost}; - -/// Tier value the server writes for classifier and judge calls. -const CLASSIFIER_TIER: &str = "classifier"; - -/// Billable token counts for one model. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct ModelTokens { - pub requests: u64, - /// Input tokens exclude cache reads and writes. - pub input: u64, - /// Input tokens served from the provider cache. - pub cached_input: u64, - /// Generated tokens, including reasoning. - pub output: u64, - /// Cache-write tokens are separate from uncached input. - pub cache_write: u64, - /// One-hour writes are a subset of all cache writes. - pub cache_write_1h: u64, -} - -impl ModelTokens { - /// Every token the provider counted, cached reads included. - pub fn total(&self) -> u64 { - self.input + self.cached_input + self.cache_write + self.output - } - - fn add(&mut self, other: &Self) { - self.requests += other.requests; - self.input += other.input; - self.cached_input += other.cached_input; - self.output += other.output; - self.cache_write += other.cache_write; - self.cache_write_1h += other.cache_write_1h; - } -} - -/// Tokens for a period, split by who asked for them. -/// -/// The split matters for savings: without Switchyard the `routed` calls would -/// still have happened, but the `classifier` calls would not exist at all. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Totals { - /// Actual spending comes only from the saved cost of each call. - pub cost: CostTotals, - /// Missing or invalid answer usage keeps savings unknown after totals are merged. - pub baseline_unknown: bool, - /// Calls that served the caller, keyed by the model that answered. - pub routed: BTreeMap, - /// Switchyard's own classifier and judge calls, keyed by model. - pub classifier: BTreeMap, -} - -impl Totals { - fn add(&mut self, record: &Record) { - self.cost.add(record.cost); - if record.tier != CLASSIFIER_TIER { - self.baseline_unknown |= !record.has_baseline_usage(); - } - let bucket = if record.tier == CLASSIFIER_TIER { - &mut self.classifier - } else { - &mut self.routed - }; - bucket - .entry(record.model.clone()) - .or_default() - .add(&record.tokens()); - } - - fn merge(&mut self, other: &Self) { - self.cost.merge(&other.cost); - self.baseline_unknown |= other.baseline_unknown; - for (model, tokens) in &other.routed { - self.routed.entry(model.clone()).or_default().add(tokens); - } - for (model, tokens) in &other.classifier { - self.classifier - .entry(model.clone()) - .or_default() - .add(tokens); - } - } - - /// Calls made on the caller's behalf. - pub fn requests(&self) -> u64 { - self.routed.values().map(|tokens| tokens.requests).sum() - } - - /// Every token spent, Switchyard's own routing overhead included. - pub fn tokens(&self) -> u64 { - self.routed - .values() - .chain(self.classifier.values()) - .map(ModelTokens::total) - .sum() - } - - /// True when nothing was recorded. - pub fn is_empty(&self) -> bool { - self.routed.is_empty() && self.classifier.is_empty() - } -} - -/// What the menu shows. -#[derive(Clone, Debug, Default)] -pub struct Usage { - pub today: Totals, - pub week: Totals, -} - -/// Fields the rollup needs from one routing log line. -#[derive(Deserialize)] -struct Record { - ts: String, - model: String, - #[serde(default)] - tier: String, - #[serde(default)] - prompt_tokens: Option, - #[serde(default)] - cached_tokens: Option, - #[serde(default)] - completion_tokens: Option, - #[serde(default)] - cache_creation_tokens: Option, - #[serde(default)] - cache_creation_1h_tokens: Option, - // A missing field requires migration; explicit null is a valid unknown cost. - #[serde(deserialize_with = "Option::deserialize")] - cost: Option, -} - -impl Record { - // Each record must be valid before aggregation can conceal inconsistent cache counts. - fn has_baseline_usage(&self) -> bool { - let Some(input) = self.prompt_tokens else { - return false; - }; - self.completion_tokens.is_some() - && input - .checked_sub(self.cached_tokens.unwrap_or(0)) - .and_then(|input| input.checked_sub(self.cache_creation_tokens.unwrap_or(0))) - .is_some() - && self.cache_creation_1h_tokens.unwrap_or(0) <= self.cache_creation_tokens.unwrap_or(0) - } - - fn tokens(&self) -> ModelTokens { - ModelTokens { - requests: 1, - // `prompt_tokens` is the whole input, cache reads included. - input: self - .prompt_tokens - .unwrap_or(0) - .saturating_sub(self.cached_tokens.unwrap_or(0)) - .saturating_sub(self.cache_creation_tokens.unwrap_or(0)), - cached_input: self.cached_tokens.unwrap_or(0), - // Completion tokens already include reasoning tokens. - output: self.completion_tokens.unwrap_or(0), - cache_write: self.cache_creation_tokens.unwrap_or(0), - cache_write_1h: self.cache_creation_1h_tokens.unwrap_or(0), - } - } -} - -/// Keeps daily totals and reads only newly completed log lines. -#[derive(Default)] -pub struct Reader { - metadata: Option, - offset: u64, - days: BTreeMap, -} - -impl Reader { - /// A missing log has no usage. Replacement or truncation starts fresh. - pub fn read(&mut self, path: &Path, today: NaiveDate) -> std::io::Result { - let mut file = match File::open(path) { - Ok(file) => file, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - *self = Self::default(); - return Ok(Usage::default()); - } - Err(error) => return Err(error), - }; - let metadata = file.metadata()?; - if metadata.len() < self.offset - || !self - .metadata - .as_ref() - .is_some_and(|previous| is_same_file(previous, &metadata)) - { - self.offset = 0; - self.days.clear(); - } - self.metadata = Some(metadata); - file.seek(SeekFrom::Start(self.offset))?; - - let week_start = today - chrono::Duration::days(6); - self.days.retain(|day, _| *day >= week_start); - let mut reader = BufReader::new(file); - let mut line = Vec::new(); - loop { - line.clear(); - let bytes = reader.read_until(b'\n', &mut line)?; - // Leave an unfinished line at the offset so the next refresh retries it. - if line.last() != Some(&b'\n') { - break; - } - let record = match serde_json::from_slice::(&line) { - Ok(record) => record, - Err(error) if error.is_data() => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!( - "invalid routing record: {error}; migrate older logs before reading them" - ), - )); - } - Err(_) => { - self.offset += bytes as u64; - continue; - } - }; - self.offset += bytes as u64; - let Ok(ts) = DateTime::parse_from_rfc3339(&record.ts) else { - continue; - }; - let day = ts.with_timezone(&Local).date_naive(); - if day >= week_start { - self.days.entry(day).or_default().add(&record); - } - } - let mut usage = Usage::default(); - for (day, totals) in &self.days { - if *day == today { - usage.today = totals.clone(); - } - if *day <= today { - usage.week.merge(totals); - } - } - Ok(usage) - } -} - -fn is_same_file(left: &Metadata, right: &Metadata) -> bool { - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - left.dev() == right.dev() && left.ino() == right.ino() - } - #[cfg(not(unix))] - { - left.created() - .ok() - .is_some_and(|created| right.created().ok() == Some(created)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn read(path: &Path, today: NaiveDate) -> std::io::Result { - Reader::default().read(path, today) - } - - fn date(text: &str) -> NaiveDate { - text.parse().expect("valid date") - } - - /// Builds a log line stamped at a local time, mirroring how the server - /// writes timestamps for events that happened in the local day. - fn line(local_ts: &str, model: &str, tier: &str, prompt: u64, completion: u64) -> String { - let offset = Local::now().offset().to_string(); - format!( - r#"{{"ts":"{local_ts}{offset}","model":"{model}","tier":"{tier}","prompt_tokens":{prompt},"cached_tokens":0,"completion_tokens":{completion},"reasoning_tokens":0,"cost":null}}"# - ) - } - - fn log(contents: &str) -> (tempfile::TempDir, std::path::PathBuf) { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("routing.jsonl"); - std::fs::write(&path, contents).expect("write log"); - (dir, path) - } - - #[test] - fn splits_routed_and_classifier_tokens() { - let (_dir, path) = log(&format!( - "{}\n{}\n", - line("2026-09-28T10:00:00.000", "luna", "", 1_000, 200), - line("2026-09-28T10:00:01.000", "terra", "classifier", 300, 10), - )); - - let usage = read(&path, date("2026-09-28")).expect("read"); - - assert_eq!( - usage.today.requests(), - 1, - "classifier calls are not requests" - ); - assert_eq!(usage.today.routed["luna"].input, 1_000); - assert_eq!(usage.today.classifier["terra"].input, 300); - assert_eq!(usage.today.tokens(), 1_000 + 200 + 300 + 10); - } - - #[test] - fn separates_cache_reads_from_full_price_input() { - let record: Record = serde_json::from_str( - r#"{"ts":"2026-09-28T10:00:00.000Z","model":"m","prompt_tokens":1000,"cached_tokens":600,"completion_tokens":50,"reasoning_tokens":25,"cost":null}"#, - ) - .expect("parse record"); - let tokens = record.tokens(); - assert_eq!(tokens.input, 400); - assert_eq!(tokens.cached_input, 600); - assert_eq!(tokens.output, 50); - assert_eq!(tokens.total(), 1_050); - } - - #[test] - fn counts_completion_tokens_once_and_prices_them() { - let record: Record = serde_json::from_str( - r#"{"ts":"2026-09-28T10:00:00.000Z","model":"m","prompt_tokens":12,"cached_tokens":0,"cache_creation_tokens":10,"completion_tokens":5,"reasoning_tokens":3,"cost":{"usd":0.000022,"source":"provider_reported"}}"#, - ) - .expect("parse record"); - - let tokens = record.tokens(); - - assert_eq!(tokens.input, 2); - assert_eq!(tokens.output, 5); - assert_eq!(tokens.total(), 17); - let mut totals = Totals::default(); - totals.add(&record); - let prices = switchyard_costs::Pricing::parse( - br#"{"m":{"input":1,"cache_read":1,"cache_write":1,"cache_write_1h":3,"output":2}}"#, - ) - .expect("rates"); - let savings = crate::pricing::estimate(&totals, &prices, "m").expect("priced"); - assert!((savings.actual - 0.000022).abs() < 1e-12); - // A known zero charge still needs valid usage for the hypothetical baseline. - for fields in [ - r#""prompt_tokens":null,"completion_tokens":5"#, - r#""completion_tokens":5"#, - r#""prompt_tokens":2,"cached_tokens":3,"completion_tokens":5"#, - r#""prompt_tokens":20,"cache_creation_tokens":2,"cache_creation_1h_tokens":3,"completion_tokens":5"#, - ] { - let record: Record = serde_json::from_str(&format!( - r#"{{"ts":"2026-09-28T10:00:00Z","model":"m",{fields},"cost":{{"usd":0,"source":"provider_reported"}}}}"# - )).expect("record with incomplete baseline usage"); - let mut incomplete = Totals::default(); - incomplete.add(&record); - incomplete.merge(&totals); - assert_eq!(incomplete.cost.total_usd, Some(0.000022)); - assert!(crate::pricing::estimate(&incomplete, &prices, "m").is_none()); - } - } - - #[test] - fn the_week_covers_seven_days() { - let mut contents = String::new(); - for day in 18..=28 { - contents.push_str(&line( - &format!("2026-09-{day:02}T10:00:00.000"), - "luna", - "", - 100, - 10, - )); - contents.push('\n'); - } - let (_dir, path) = log(&contents); - - let usage = read(&path, date("2026-09-28")).expect("read"); - - assert_eq!(usage.today.requests(), 1); - assert_eq!(usage.week.requests(), 7); - } - - #[test] - fn skips_lines_it_cannot_parse() { - // The trailing line has no newline yet, as when the server is mid-write. - let (_dir, path) = log(&format!( - "not json\n{}\n{{\"ts\":\"2026-09-2", - line("2026-09-28T10:00:00.000", "luna", "", 100, 10), - )); - - let usage = read(&path, date("2026-09-28")).expect("read"); - - assert_eq!(usage.today.requests(), 1); - } - - #[test] - fn reads_appends_once_and_retries_an_unfinished_line() { - use std::io::Write; - - let first = line("2026-09-28T10:00:00.000", "luna", "", 100, 10); - let second = line("2026-09-28T10:00:01.000", "terra", "classifier", 30, 1); - let (_dir, path) = log(&format!("{first}\n")); - let today = date("2026-09-28"); - let mut reader = Reader::default(); - assert_eq!(reader.read(&path, today).expect("read").today.tokens(), 110); - assert_eq!( - reader.read(&path, today).expect("reread").today.tokens(), - 110 - ); - - let mut file = std::fs::OpenOptions::new() - .append(true) - .open(&path) - .expect("open"); - let split = second.len() / 2; - file.write_all(&second.as_bytes()[..split]) - .expect("partial append"); - assert_eq!( - reader - .read(&path, today) - .expect("partial read") - .today - .tokens(), - 110 - ); - writeln!(file, "{}", &second[split..]).expect("finish append"); - let usage = reader.read(&path, today).expect("read append"); - assert_eq!(usage.today.tokens(), 141); - assert_eq!(usage.today.requests(), 1); - assert_eq!(usage.today.classifier["terra"].requests, 1); - } - - #[test] - fn drops_old_days_without_rereading_the_log() { - let (_dir, path) = log(&format!( - "{}\n{}\n", - line("2026-09-22T10:00:00.000", "luna", "", 100, 10), - line("2026-09-28T10:00:00.000", "luna", "", 100, 10), - )); - let mut reader = Reader::default(); - assert_eq!( - reader - .read(&path, date("2026-09-28")) - .expect("read") - .week - .requests(), - 2 - ); - let usage = reader.read(&path, date("2026-09-29")).expect("next day"); - assert_eq!(usage.today.requests(), 0); - assert_eq!(usage.week.requests(), 1); - } - - #[test] - fn resets_totals_when_the_log_is_replaced_or_truncated() { - let (_dir, path) = log(&format!( - "{}\n{}\n", - line("2026-09-28T10:00:00.000", "luna", "", 100, 10), - line("2026-09-28T10:00:01.000", "luna", "", 100, 10), - )); - let mut reader = Reader::default(); - let today = date("2026-09-28"); - assert_eq!(reader.read(&path, today).expect("read").today.requests(), 2); - let replacement = format!("{}\n", line("2026-09-28T11:00:00.000", "sol", "", 200, 20)); - std::fs::write(&path, &replacement).expect("truncate"); - let usage = reader.read(&path, today).expect("read truncated"); - assert_eq!(usage.today.requests(), 1); - assert_eq!(usage.today.tokens(), 220); - - std::fs::rename(&path, path.with_extension("old")).expect("rotate"); - std::fs::write(&path, replacement.repeat(3)).expect("replace"); - let usage = reader.read(&path, today).expect("read replacement"); - assert_eq!(usage.today.requests(), 3); - assert_eq!(usage.today.tokens(), 660); - } - - #[test] - fn a_missing_log_is_not_an_error() { - let dir = tempfile::tempdir().expect("tempdir"); - - let usage = read(&dir.path().join("absent.jsonl"), date("2026-09-28")).expect("read"); - - assert!(usage.today.is_empty()); - } -} diff --git a/crates/switchyard-menubar/src/summary.rs b/crates/switchyard-menubar/src/summary.rs deleted file mode 100644 index 42df97830..000000000 --- a/crates/switchyard-menubar/src/summary.rs +++ /dev/null @@ -1,284 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Turns totals and prices into the rows the menu shows. - -use crate::config::Config; -use crate::health::ServerStatus; -use crate::pricing::{Savings, estimate}; -use crate::rollup::{Totals, Usage}; - -/// Most model rows shown, so the menu stays short. -const MAX_MODEL_ROWS: usize = 5; - -/// One line of the menu. Rows are informational; the actions are fixed. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Row { - Separator, - Label(String), -} - -fn label(text: impl Into) -> Row { - Row::Label(text.into()) -} - -/// Builds the menu body for the current day and trailing week. -pub fn build(status: ServerStatus, usage: &Usage, config: &Config) -> Vec { - let state = match status { - ServerStatus::Running => "running", - ServerStatus::Stopped => "not responding", - }; - let mut rows = vec![ - label(format!("Server: {state} · {}", config.server_url)), - Row::Separator, - ]; - - if usage.week.is_empty() { - rows.push(label("No requests recorded yet")); - return rows; - } - - rows.extend(period("Today", &usage.today, config)); - rows.push(Row::Separator); - rows.extend(period("This week", &usage.week, config)); - - let models = model_shares(&usage.week); - if !models.is_empty() { - rows.push(Row::Separator); - rows.push(label("This week by model")); - rows.extend(models); - } - - if estimate(&usage.week, &config.prices, &config.baseline_model).is_none() { - rows.push(Row::Separator); - rows.push(label( - "Savings need complete recorded costs, answer usage, and baseline rates", - )); - } - rows -} - -fn period(name: &str, totals: &Totals, config: &Config) -> Vec { - let requests = totals.requests(); - let mut rows = vec![label(format!( - "{name} — {requests} request{} · {} tokens", - if requests == 1 { "" } else { "s" }, - tokens(totals.tokens()) - ))]; - if let Some(savings) = estimate(totals, &config.prices, &config.baseline_model) { - rows.push(label(format!(" Estimated savings {}", saved(&savings)))); - } - rows -} - -fn saved(savings: &Savings) -> String { - let amount = money(savings.saved()); - match savings.percent() { - Some(percent) => format!("{amount} ({percent:.0}% of {})", money(savings.baseline)), - None => amount, - } -} - -fn model_shares(week: &Totals) -> Vec { - let total: u64 = week.routed.values().map(|tokens| tokens.total()).sum(); - if total == 0 { - return Vec::new(); - } - let mut models: Vec<_> = week - .routed - .iter() - .map(|(model, tokens)| (model, tokens.total())) - .collect(); - // Largest share first; model id breaks ties so the order is stable. - models.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(right.0))); - models - .into_iter() - .take(MAX_MODEL_ROWS) - .map(|(model, count)| { - label(format!( - " {model} — {:.0}%", - count as f64 / total as f64 * 100.0 - )) - }) - .collect() -} - -/// Formats a token count compactly, since exact totals are not useful here. -fn tokens(value: u64) -> String { - match value { - 0..=9_999 => value.to_string(), - 10_000..=999_999 => format!("{:.0}K", value as f64 / 1_000.0), - 1_000_000..=999_999_999 => format!("{:.1}M", value as f64 / 1_000_000.0), - _ => format!("{:.1}B", value as f64 / 1_000_000_000.0), - } -} - -/// Formats dollars, keeping the sign readable when routing cost more than it saved. -fn money(value: f64) -> String { - if value < 0.0 { - format!("-${:.2}", -value) - } else { - format!("${value:.2}") - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::rollup::ModelTokens; - use switchyard_costs::{CostSource, Pricing, RecordedCost}; - - fn counted(input: u64, output: u64, requests: u64) -> ModelTokens { - ModelTokens { - requests, - input, - cached_input: 0, - output, - ..ModelTokens::default() - } - } - - fn config(prices: Pricing) -> Config { - Config { - baseline_model: "sol".to_string(), - prices, - ..Config::default() - } - } - - fn priced() -> Pricing { - Pricing::parse(br#"{"sol":{"input":1.25,"cache_read":1.25,"cache_write":1.25,"output":10},"luna":{"input":0.25,"cache_read":0.25,"cache_write":0.25,"output":2}}"#).expect("rates") - } - - fn labels(rows: &[Row]) -> Vec { - rows.iter() - .filter_map(|row| match row { - Row::Label(text) => Some(text.clone()), - Row::Separator => None, - }) - .collect() - } - - fn usage_of(today: Totals, week: Totals) -> Usage { - Usage { today, week } - } - - #[test] - fn shows_requests_tokens_and_savings_for_both_periods() { - let mut today = Totals::default(); - today - .routed - .insert("luna".to_string(), counted(1_000_000, 100_000, 3)); - let mut week = Totals::default(); - week.routed - .insert("luna".to_string(), counted(4_000_000, 400_000, 12)); - - today.cost.add(Some(RecordedCost { - usd: 0.45, - source: CostSource::Estimated, - })); - week.cost.add(Some(RecordedCost { - usd: 1.80, - source: CostSource::Estimated, - })); - let rows = build( - ServerStatus::Running, - &usage_of(today, week), - &config(priced()), - ); - let labels = labels(&rows); - - assert_eq!(labels[0], "Server: running · http://127.0.0.1:4123"); - assert_eq!(labels[1], "Today — 3 requests · 1.1M tokens"); - assert_eq!(labels[2], " Estimated savings $1.80 (80% of $2.25)"); - assert_eq!(labels[3], "This week — 12 requests · 4.4M tokens"); - assert_eq!(labels[4], " Estimated savings $7.20 (80% of $9.00)"); - } - - #[test] - fn hides_savings_and_explains_why_when_prices_are_missing() { - let mut week = Totals::default(); - week.routed - .insert("luna".to_string(), counted(1_000, 100, 1)); - - week.classifier - .insert("sol".to_string(), counted(100, 0, 1)); - week.cost.add(None); - for prices in [Pricing::default(), priced()] { - let rows = build( - ServerStatus::Running, - &usage_of(week.clone(), week.clone()), - &config(prices), - ); - let labels = labels(&rows); - assert!( - labels - .iter() - .all(|label| !label.contains("Estimated savings")) - ); - assert!(labels.iter().any(|label| label.contains("costs"))); - } - } - - #[test] - fn ranks_models_by_token_share() { - let mut week = Totals::default(); - week.routed - .insert("luna".to_string(), counted(700_000, 0, 7)); - week.routed - .insert("sol".to_string(), counted(300_000, 0, 3)); - - let rows = build( - ServerStatus::Running, - &usage_of(Totals::default(), week), - &config(priced()), - ); - let models: Vec<_> = labels(&rows) - .into_iter() - .filter(|label| label.starts_with(" ") && label.ends_with('%')) - .collect(); - - assert_eq!(models, vec![" luna — 70%", " sol — 30%"]); - } - - #[test] - fn reports_a_stopped_server_with_no_traffic() { - let rows = build(ServerStatus::Stopped, &Usage::default(), &config(priced())); - let labels = labels(&rows); - - assert!(labels[0].starts_with("Server: not responding")); - assert_eq!(labels[1], "No requests recorded yet"); - } - - #[test] - fn shows_a_loss_when_routing_overhead_exceeds_the_saving() { - let mut today = Totals::default(); - today.routed.insert("sol".to_string(), counted(1_000, 0, 1)); - today - .classifier - .insert("sol".to_string(), counted(1_000_000, 0, 1)); - - today.cost.add(Some(RecordedCost { - usd: 1.25125, - source: CostSource::Estimated, - })); - let rows = build( - ServerStatus::Running, - &usage_of(today.clone(), today), - &config(priced()), - ); - - assert!(labels(&rows)[2].starts_with(" Estimated savings -$1.25")); - } - - #[test] - fn formats_tokens_and_money_for_reading() { - assert_eq!(tokens(412), "412"); - assert_eq!(tokens(12_345), "12K"); - assert_eq!(tokens(1_400_000), "1.4M"); - assert_eq!(tokens(9_700_000_000), "9.7B"); - - assert_eq!(money(2.181), "$2.18"); - assert_eq!(money(-0.125), "-$0.12"); - } -} diff --git a/crates/switchyard-menubar/src/tray.rs b/crates/switchyard-menubar/src/tray.rs deleted file mode 100644 index 7a39c87da..000000000 --- a/crates/switchyard-menubar/src/tray.rs +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! The macOS status item: event loop, menu, and the menu's actions. -//! -//! Everything platform-specific lives here, so the rest of the crate builds -//! and is tested on any target. - -use std::path::PathBuf; -use std::process::Command; -use std::thread; -use std::time::{Duration, Instant}; - -use objc2::rc::autoreleasepool; -use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEventMask}; -use objc2_foundation::{MainThreadMarker, NSDate, NSDefaultRunLoopMode}; -use tray_icon::menu::{IsMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem}; -use tray_icon::{Icon, TrayIconBuilder}; - -use crate::app::refresh; -use crate::config::Config; -use crate::icon; -use crate::summary::Row; - -const RESTART: &str = "restart-server"; -const OPEN_CONFIG: &str = "open-config"; -const OPEN_SETTINGS: &str = "open-settings"; -const QUIT: &str = "quit"; - -/// How long the loop blocks waiting for a UI event before checking the clock. -const EVENT_POLL: f64 = 0.1; - -/// Runs the status item until the user quits. -pub fn run(config: Config, settings_path: PathBuf) -> Result<(), String> { - let mtm = MainThreadMarker::new().ok_or("the menu bar must run on the main thread")?; - let ns_app = NSApplication::sharedApplication(mtm); - // Accessory keeps the process out of the Dock and the app switcher. - ns_app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); - - let mut log = crate::rollup::Reader::default(); - let glyph = Icon::from_rgba(icon::glyph(), icon::SIZE, icon::SIZE) - .map_err(|error| format!("build icon: {error}"))?; - let tray = TrayIconBuilder::new() - .with_icon(glyph) - .with_icon_as_template(true) - .with_tooltip("Switchyard") - .with_menu(Box::new(menu(&refresh(&config, &mut log))?)) - .build() - .map_err(|error| format!("create status item: {error}"))?; - - ns_app.finishLaunching(); - - let interval = Duration::from_secs(config.refresh_seconds.max(5)); - let mut next = Instant::now() + interval; - let mut actions = Vec::new(); - loop { - let should_quit = autoreleasepool(|_| -> Result { - pump_events(&ns_app); - - let mut redraw = false; - while let Ok(event) = MenuEvent::receiver().try_recv() { - let action = match event.id.as_ref() { - QUIT => return Ok(true), - RESTART => { - let label = config.launchd_label.clone(); - thread::spawn(move || restart_server(&label)) - } - OPEN_CONFIG | OPEN_SETTINGS => { - let path = if event.id.as_ref() == OPEN_CONFIG { - config.config_file.clone() - } else { - settings_path.clone() - }; - thread::spawn(move || open(&path)) - } - _ => continue, - }; - actions.push(action); - } - for index in (0..actions.len()).rev() { - if actions[index].is_finished() { - report( - actions - .swap_remove(index) - .join() - .unwrap_or_else(|_| Err("menu action thread panicked".to_string())), - ); - redraw = true; - } - } - - if redraw || Instant::now() >= next { - tray.set_menu(Some(Box::new(menu(&refresh(&config, &mut log))?))); - next = Instant::now() + interval; - } - Ok(false) - })?; - if should_quit { - return Ok(()); - } - } -} - -/// Restarts the server's LaunchAgent. -fn restart_server(label: &str) -> Result<(), String> { - let uid = command("id", &["-u"])?; - let target = format!("gui/{}/{}", uid.trim(), label); - command("launchctl", &["kickstart", "-k", &target]).map(|_| ()) -} - -/// Opens a config file in the user's editor. -fn open(path: &std::path::Path) -> Result<(), String> { - command("open", &["-t", &path.display().to_string()]).map(|_| ()) -} - -/// Runs a command, returning its stdout or a message naming what failed. -fn command(program: &str, args: &[&str]) -> Result { - let output = Command::new(program) - .args(args) - .output() - .map_err(|error| format!("run {program}: {error}"))?; - if !output.status.success() { - return Err(format!( - "{program} {}: {}", - args.join(" "), - String::from_utf8_lossy(&output.stderr).trim() - )); - } - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) -} - -fn report(result: Result<(), String>) { - if let Err(error) = result { - eprintln!("switchyard-menubar: {error}"); - } -} - -/// Builds the whole menu: the summary rows, then the fixed actions. -fn menu(rows: &[Row]) -> Result { - let menu = Menu::new(); - let append = |item: &dyn IsMenuItem| { - menu.append(item) - .map_err(|error| format!("build menu: {error}")) - }; - - for row in rows { - match row { - Row::Separator => append(&PredefinedMenuItem::separator())?, - // Rows are labels, not commands, so they are disabled. - Row::Label(text) => append(&MenuItem::new(text, false, None))?, - } - } - append(&PredefinedMenuItem::separator())?; - for (id, text) in [ - (RESTART, "Restart server"), - (OPEN_CONFIG, "Open server config…"), - (OPEN_SETTINGS, "Open menu bar settings…"), - (QUIT, "Quit"), - ] { - append(&MenuItem::with_id(id, text, true, None))?; - } - Ok(menu) -} - -/// Drains pending AppKit events, blocking briefly when there are none. -fn pump_events(ns_app: &NSApplication) { - let mut expiration = Some(NSDate::dateWithTimeIntervalSinceNow(EVENT_POLL)); - while let Some(event) = unsafe { - ns_app.nextEventMatchingMask_untilDate_inMode_dequeue( - NSEventMask::Any, - expiration.as_deref(), - NSDefaultRunLoopMode, - true, - ) - } { - ns_app.sendEvent(&event); - // Only the first wait blocks; the rest drain what is already queued. - expiration = Some(NSDate::distantPast()); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reports_which_command_failed() { - let error = command("switchyard-does-not-exist", &[]).expect_err("missing program"); - - assert!(error.contains("switchyard-does-not-exist")); - } -} diff --git a/crates/switchyard-server/README.md b/crates/switchyard-server/README.md index ce468ef08..fd71a5c8f 100644 --- a/crates/switchyard-server/README.md +++ b/crates/switchyard-server/README.md @@ -158,8 +158,7 @@ at startup, and saved costs do not change when prices change. An unreadable or i `--pricing-file` stops startup; the server does not use bundled rates instead. The [pi guide](../../docs/integrations/pi.md#estimate-the-cost) explains how to supply -rates for your own models. The menubar reads the same file through its `pricing_file` -setting when you override the bundled rates; its actual spending comes from recorded costs. +rates for your own models. Clients can send `x-switchyard-origin: codex-cli` (or another client label) to include an `origin` field in each routing record. Missing, empty, or non-text header values produce diff --git a/scripts/macos/common.sh b/scripts/macos/common.sh deleted file mode 100644 index 9978601f4..000000000 --- a/scripts/macos/common.sh +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Paths, markers, and helpers shared by install.sh and uninstall.sh. -# The markers must match between the two, which is why they live here. - -# shellcheck source=scripts/common.sh -source "$(dirname "${BASH_SOURCE[0]}")/../common.sh" - -SERVER_LABEL="com.nvidia.switchyard.server" -MENUBAR_LABEL="com.nvidia.switchyard.menubar" -LAUNCH_AGENTS="$HOME/Library/LaunchAgents" -CODEX_CONFIG="$CODEX_DIR/config.toml" -# Codex reads `--profile sy` from its own file next to config.toml. A -# [profiles.sy] table in config.toml is rejected outright as legacy config. -# Codex.app ignores profiles entirely, so it gets a whole config.toml with the -# routing inlined. Keep the generated file beside the original for reference. -CODEX_SWITCHYARD_CONFIG="$CODEX_DIR/config.sy.toml" -CODEX_DIRECT_CONFIG="$CODEX_DIR/config.toml.direct" -PROFILE_START="# >>> switchyard sy profile >>>" -PROFILE_END="# <<< switchyard sy profile <<<" - -codex_config_uses_switchyard() { - local path="$1" - [[ -f "$path" ]] || return 1 - awk ' - function is_string(value, wanted, quote, prefix, suffix) { - sub(/^[[:space:]]*/, "", value) - quote = substr(value, 1, 1) - if (quote != "\"" && quote != sprintf("%c", 39)) return 0 - prefix = quote wanted quote - if (substr(value, 1, length(prefix)) != prefix) return 0 - suffix = substr(value, length(prefix) + 1) - return suffix ~ /^[[:space:]]*(#.*)?$/ - } - BEGIN { top_level = 1 } - /^[[:space:]]*#/ { next } - /^[[:space:]]*\[/ { top_level = 0; next } - top_level && /^[[:space:]]*model_provider[[:space:]]*=/ { - value = $0 - sub(/^[^=]*=[[:space:]]*/, "", value) - if (is_string(value, "sy")) found = 1 - } - END { exit !found } - ' "$path" -} diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh deleted file mode 100755 index 8fb6e2a92..000000000 --- a/scripts/macos/install.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Installs the Switchyard background server and its menu bar companion as -# per-user LaunchAgents and sets up a `sy` Codex profile. -# -# Keeps existing server and menu bar settings and backs up sy.config.toml -# before replacing it. -# Run with --dry-run to print what would happen. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -DRY_RUN=0 -case "$#:${1:-}" in - 0:) ;; - 1:--dry-run) DRY_RUN=1 ;; - *) printf 'Usage: %s [--dry-run]\n' "$0" >&2; exit 2 ;; -esac - -# shellcheck source=scripts/macos/common.sh -source "$SCRIPT_DIR/common.sh" - -xml_escape_text() { - printf '%s' "$1" | sed \ - -e 's/&/\&/g' \ - -e 's//\>/g' -} - -toml_escape_basic_string() { - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -} - -if [[ "$(uname -s)" != "Darwin" ]]; then - say "This installer is for macOS only." >&2 - exit 1 -fi - -step "Building release binaries" -run cargo build --release --manifest-path "$REPO_ROOT/Cargo.toml" \ - -p switchyard-server -p switchyard-menubar - -step "Installing binaries into $SY_HOME/bin" -run mkdir -p "$SY_HOME/bin" "$SY_HOME/logs" -for binary in switchyard-server switchyard-menubar; do - run install -m 755 "$REPO_ROOT/target/release/$binary" "$SY_HOME/bin/$binary" -done - -step "Writing server config" -# Keep macOS and Linux on the same routing defaults. -write_once "$SY_HOME/composite.toml" < "$REPO_ROOT/scripts/config/composite.toml" - -step "Writing menu bar settings" -# Existing price files override the bundled table for both applications. -TOML_SY_HOME="$(toml_escape_basic_string "$SY_HOME")" -XML_SY_HOME="$(xml_escape_text "$SY_HOME")" -PRICING_SETTING="" -PRICING_XML="" -SERVER_ARGS=( - --config "$SY_HOME/composite.toml" - --routing-log-file "$SY_HOME/routing.jsonl" -) -if [[ -e "$SY_HOME/prices.json" ]]; then - SERVER_ARGS+=(--pricing-file "$SY_HOME/prices.json") - PRICING_SETTING="pricing_file = \"$TOML_SY_HOME/prices.json\"" - PRICING_XML="--pricing-file - $XML_SY_HOME/prices.json" -fi -write_once "$SY_HOME/menubar.toml" < - - - - Label - $SERVER_LABEL - ProgramArguments - - $XML_SY_HOME/bin/switchyard-server - --config - $XML_SY_HOME/composite.toml - --host - 127.0.0.1 - --port - $SY_PORT - --routing-log-file - $XML_SY_HOME/routing.jsonl - $PRICING_XML - - RunAtLoad - - KeepAlive - - ProcessType - Background - StandardOutPath - $XML_SY_HOME/logs/server.log - StandardErrorPath - $XML_SY_HOME/logs/server.err.log - EnvironmentVariables - - RUST_LOG - info - - - -EOF - -# SuccessfulExit false means the Quit menu item stays quit, while a crash is -# still restarted. Aqua-only, since there is no menu bar without a login session. -write_always "$LAUNCH_AGENTS/$MENUBAR_LABEL.plist" < - - - - Label - $MENUBAR_LABEL - ProgramArguments - - $XML_SY_HOME/bin/switchyard-menubar - $XML_SY_HOME/menubar.toml - - RunAtLoad - - KeepAlive - - SuccessfulExit - - - LimitLoadToSessionType - Aqua - StandardOutPath - $XML_SY_HOME/logs/menubar.log - StandardErrorPath - $XML_SY_HOME/logs/menubar.err.log - - -EOF - -step "Loading LaunchAgents" -for label in "$SERVER_LABEL" "$MENUBAR_LABEL"; do - if (( DRY_RUN )); then - say " would reload gui/$UID/$label" - continue - fi - launchctl bootout "gui/$UID/$label" 2>/dev/null || true - # bootout returns before the job leaves the domain, and bootstrapping a - # service that is still shutting down fails with an I/O error. - for _ in $(seq 50); do - launchctl print "gui/$UID/$label" >/dev/null 2>&1 || break - sleep 0.1 - done - launchctl bootstrap "gui/$UID" "$LAUNCH_AGENTS/$label.plist" - # macOS can defer RunAtLoad jobs until an explicit start is requested. - launchctl kickstart "gui/$UID/$label" - say " started $label" -done - -step "Adding the sy Codex profile" -# This profile only changes which router answers. Approval and sandbox -# settings are deliberately left out, so the profile cannot loosen how Codex -# asks before it acts. Set those yourself if you want them. -sed "s/@SY_PORT@/$SY_PORT/g" "$REPO_ROOT/scripts/config/codex.sy.toml" | - write_with_backup "$CODEX_PROFILE_CONFIG" - -step "Done" -say "Server: http://127.0.0.1:$SY_PORT (logs in $SY_HOME/logs)" -say "Use it with: codex -p sy (requires Codex CLI 0.134.0 or newer)" -say "Settings: $SY_HOME/menubar.toml" -say "Look for the Switchyard glyph in the menu bar." diff --git a/scripts/macos/uninstall.sh b/scripts/macos/uninstall.sh deleted file mode 100755 index 66f670e17..000000000 --- a/scripts/macos/uninstall.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Removes what install.sh added: the two LaunchAgents, the `sy` Codex profile, -# and the codex alias. Your config, routing log, and binaries stay put; the -# paths are printed so you can delete them yourself. -# -# Run with --dry-run to print what would happen. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -DRY_RUN=0 -case "$#:${1:-}" in - 0:) ;; - 1:--dry-run) DRY_RUN=1 ;; - *) printf 'Usage: %s [--dry-run]\n' "$0" >&2; exit 2 ;; -esac - -# shellcheck source=scripts/macos/common.sh -source "$SCRIPT_DIR/common.sh" - -step "Unloading LaunchAgents" -for label in "$SERVER_LABEL" "$MENUBAR_LABEL"; do - if (( DRY_RUN )); then - say " would unload gui/$UID/$label and delete its plist" - else - launchctl bootout "gui/$UID/$label" 2>/dev/null || true - rm -f "$LAUNCH_AGENTS/$label.plist" - say " unloaded $label" - fi -done - -step "Removing the sy Codex profile" -remove_file "$CODEX_PROFILE_CONFIG" -remove_file "$CODEX_SWITCHYARD_CONFIG" -# Older installs of this script put the profile in config.toml instead. -strip_block "$CODEX_CONFIG" "$PROFILE_START" "$PROFILE_END" "the legacy sy profile" || - say " no legacy profile in $CODEX_CONFIG" - -step "Unrouting Codex.app" -if [[ -f "$CODEX_DIRECT_CONFIG" ]]; then - if (( DRY_RUN )); then - say " would preserve $CODEX_CONFIG before restoring $CODEX_DIRECT_CONFIG" - else - backup="" - if [[ -f "$CODEX_CONFIG" ]]; then - backup="$(mktemp "$CODEX_CONFIG.switchyard-current.XXXXXX")" - cp "$CODEX_CONFIG" "$backup" - fi - cp "$CODEX_DIRECT_CONFIG" "$CODEX_CONFIG" - rm -f "$CODEX_DIRECT_CONFIG" - say " restored the original config.toml${backup:+; preserved the current file at $backup}" - fi -elif codex_config_uses_switchyard "$CODEX_CONFIG"; then - say " config.toml is routed but $CODEX_DIRECT_CONFIG is missing; edit it by hand" -else - remove_file "$CODEX_DIRECT_CONFIG" -fi - -step "Removing the codex alias" -for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do - strip_block "$rc" "$ALIAS_START" "$ALIAS_END" "the codex alias" || - say " no alias in $rc" -done - -step "Done" -say "Left in place, delete them if you want:" -say " $SY_HOME (binaries, config, routing log, logs)" -say " $CODEX_CONFIG.switchyard-current.* (configs preserved during restore)" -say " $CODEX_PROFILE_CONFIG.switchyard-backup.* (profile backups)" diff --git a/tests/test_macos_install.py b/tests/test_macos_install.py deleted file mode 100644 index ccef74a2b..000000000 --- a/tests/test_macos_install.py +++ /dev/null @@ -1,299 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import os -import shutil -import subprocess - -try: - import tomllib -except ModuleNotFoundError: # Python 3.10 - import tomli as tomllib -import xml.etree.ElementTree as ET -from pathlib import Path - -import pytest - -REPO = Path(__file__).resolve().parents[1] - - -@pytest.fixture -def setup(tmp_path): - scripts = tmp_path / "repo" / "scripts" / "macos" - shutil.copytree(REPO / "scripts" / "macos", scripts) - shutil.copy(REPO / "scripts" / "common.sh", scripts.parent / "common.sh") - shutil.copytree(REPO / "scripts" / "config", scripts.parent / "config") - home = tmp_path / "home" - home.mkdir() - bin_dir = tmp_path / "bin" - bin_dir.mkdir() - switchyard_home = home / "Switchyard & Routing " - env = { - **os.environ, - "HOME": str(home), - "SY_HOME": str(switchyard_home), - "CODEX_HOME": str(home / ".codex"), - "TMPDIR": str(tmp_path), - "PATH": f"{bin_dir}:/usr/bin:/bin", - } - stubs = { - "cargo": "exit 0\n", - "uname": "echo Darwin\n", - "install": 'printf "#!/bin/sh\\nexit 0\\n" > "$4"\nchmod +x "$4"\n', - "launchctl": '[[ "$1" != print ]]\n', - } - for name, body in stubs.items(): - stub = bin_dir / name - stub.write_text("#!/bin/bash\nset -eu\n" + body) - stub.chmod(0o755) - return scripts, home, switchyard_home, env - - -def run(setup, script): - scripts, _, _, env = setup - return subprocess.run( - ["bash", str(scripts / script)], - env=env, - capture_output=True, - text=True, - timeout=10, - ) - - -def read_config(path): - return path.read_text() - - -def test_install_starts_each_launch_agent_after_loading_it(setup): - _, _, _, env = setup - command_log = Path(env["TMPDIR"]) / "launchctl.log" - stub = Path(env["PATH"].split(":")[0]) / "launchctl" - stub.write_text( - '#!/bin/bash\nset -eu\n' - f'printf "%s\\n" "$*" >> "{command_log}"\n' - '[[ "$1" != print ]]\n' - ) - - result = run(setup, "install.sh") - - assert result.returncode == 0, result.stderr - commands = command_log.read_text().splitlines() - for label in ("com.nvidia.switchyard.server", "com.nvidia.switchyard.menubar"): - bootstrap = next( - index for index, command in enumerate(commands) - if command.startswith("bootstrap ") and command.endswith(f"/{label}.plist") - ) - domain = commands[bootstrap].split()[1] - assert commands[bootstrap + 1] == f"kickstart {domain}/{label}" - - -def test_install_escapes_switchyard_path_in_launch_agent(setup): - _, home, switchyard_home, _ = setup - result = run(setup, "install.sh") - assert result.returncode == 0, result.stderr - plist = home / "Library" / "LaunchAgents" / "com.nvidia.switchyard.server.plist" - root = ET.parse(plist).getroot() - entries = list(root.find("dict")) - values = {entries[index].text: entries[index + 1] for index in range(0, len(entries), 2)} - program_arguments = [element.text for element in values["ProgramArguments"]] - assert program_arguments[0] == str(switchyard_home / "bin" / "switchyard-server") - assert program_arguments[2] == str(switchyard_home / "composite.toml") - assert program_arguments[8] == str(switchyard_home / "routing.jsonl") - assert values["StandardOutPath"].text == str(switchyard_home / "logs" / "server.log") - assert values["StandardErrorPath"].text == str(switchyard_home / "logs" / "server.err.log") - - menubar = home / "Library" / "LaunchAgents" / "com.nvidia.switchyard.menubar.plist" - menubar_root = ET.parse(menubar).getroot() - menubar_entries = list(menubar_root.find("dict")) - menubar_values = { - menubar_entries[index].text: menubar_entries[index + 1] - for index in range(0, len(menubar_entries), 2) - } - menubar_arguments = [element.text for element in menubar_values["ProgramArguments"]] - assert menubar_arguments == [ - str(switchyard_home / "bin" / "switchyard-menubar"), - str(switchyard_home / "menubar.toml"), - ] - - -def test_missing_codex_config_creates_only_standalone_profile(setup): - _, home, _, _ = setup - result = run(setup, "install.sh") - assert result.returncode == 0, result.stderr - codex = home / ".codex" - expected = read_config(REPO / "scripts" / "config" / "codex.sy.toml").replace( - "@SY_PORT@", "4123" - ) - assert read_config(codex / "sy.config.toml") == expected - assert {path.name for path in codex.iterdir()} == {"sy.config.toml"} - - -def test_install_prints_profile_usage_without_editing_shell_files(setup): - _, home, _, _ = setup - zshrc = home / ".zshrc" - bashrc = home / ".bashrc" - zshrc.write_text("zsh settings\n") - bashrc.write_text("bash settings\n") - - result = run(setup, "install.sh") - - assert result.returncode == 0, result.stderr - assert "Use it with: codex -p sy" in result.stdout - assert zshrc.read_text() == "zsh settings\n" - assert bashrc.read_text() == "bash settings\n" - - -def test_menu_bar_settings_escape_sy_home_as_toml_strings(setup): - _, home, _, env = setup - switchyard_home = home / 'Switchyard "quoted" \\ folder' - env["SY_HOME"] = str(switchyard_home) - - result = run(setup, "install.sh") - - assert result.returncode == 0, result.stderr - settings = tomllib.loads((switchyard_home / "menubar.toml").read_text()) - assert settings["routing_log"] == str(switchyard_home / "routing.jsonl") - assert settings["config_file"] == str(switchyard_home / "composite.toml") - - -def test_reinstall_backs_up_profile_and_keeps_user_settings(setup): - _, _, switchyard_home, env = setup - result = run(setup, "install.sh") - assert result.returncode == 0, result.stderr - profile = Path(env["CODEX_HOME"]) / "sy.config.toml" - original = profile.read_text() - server_config = switchyard_home / "composite.toml" - menu_settings = switchyard_home / "menubar.toml" - server_config.write_text("user server settings\n") - menu_settings.write_text("user menu settings\n") - env["SY_PORT"] = "5000" - - result = run(setup, "install.sh") - - assert result.returncode == 0, result.stderr - assert "127.0.0.1:5000/v1" in profile.read_text() - backups = list(profile.parent.glob("sy.config.toml.switchyard-backup.*")) - assert len(backups) == 1 - assert backups[0].read_text() == original - assert server_config.read_text() == "user server settings\n" - assert menu_settings.read_text() == "user menu settings\n" - - -@pytest.mark.parametrize( - "provider_header", - [ - "[model_providers.\"sy\"]", - "[model_providers . 'sy']", - '[ "model_providers" . "sy" ]', - ], -) -def test_existing_codex_config_is_preserved_and_shared_templates_are_used( - setup, provider_header -): - _, home, switchyard_home, env = setup - codex = Path(env["CODEX_HOME"]) - codex.mkdir() - config = codex / "config.toml" - original = f'''theme = "dark" - -{provider_header} # old provider -name = "Old" -base_url = "http://old" - -[other] -model_provider = "sy" -''' - config.write_text(original) - result = run(setup, "install.sh") - assert result.returncode == 0, result.stderr - assert read_config(config) == original - profile = read_config(codex / "sy.config.toml") - expected = read_config(REPO / "scripts" / "config" / "codex.sy.toml").replace( - "@SY_PORT@", "4123" - ) - assert profile == expected - assert tomllib.loads(profile)["model_providers"]["sy"]["name"] == "Switchyard" - assert not (codex / "config.sy.toml").exists() - assert read_config(switchyard_home / "composite.toml") == read_config( - REPO / "scripts" / "config" / "composite.toml" - ) - assert not (codex / "config.toml.direct").exists() - - -@pytest.mark.parametrize("quote", ['"', "'"]) -def test_quoted_top_level_provider_keeps_existing_snapshot(setup, quote): - _, home, _, env = setup - codex = Path(env["CODEX_HOME"]) - codex.mkdir() - (codex / "config.toml").write_text(f"model_provider = {quote}sy{quote} # routed\n") - snapshot = codex / "config.toml.direct" - snapshot.write_text("original direct config\n") - result = run(setup, "install.sh") - assert result.returncode == 0, result.stderr - assert snapshot.read_text() == "original direct config\n" - assert (codex / "config.toml").read_text() == f"model_provider = {quote}sy{quote} # routed\n" - assert not (codex / "config.sy.toml").exists() - assert (codex / "sy.config.toml").is_file() - - -def test_uninstall_preserves_routed_config_before_restoring_snapshot(setup): - _, home, _, env = setup - codex = Path(env["CODEX_HOME"]) - codex.mkdir() - current = "model_provider = 'sy' # routed\nuser_setting = \"keep me\"\n" - (codex / "config.toml").write_text(current) - (codex / "config.toml.direct").write_text("model = \"original\"\n") - result = run(setup, "uninstall.sh") - assert result.returncode == 0, result.stderr - assert (codex / "config.toml").read_text() == 'model = "original"\n' - backups = list(codex.glob("config.toml.switchyard-current.*")) - assert len(backups) == 1 - assert backups[0].read_text() == current - - -@pytest.mark.parametrize("script", ["install.sh", "uninstall.sh"]) -@pytest.mark.parametrize( - "args", - [ - ["--dryrun"], - ["unknown"], - [""], - ["--dry-run", "extra"], - ["--dry-run", "--dry-run"], - ], -) -def test_scripts_reject_unknown_arguments_before_any_work(setup, script, args): - scripts, home, _, env = setup - profile = Path(env["CODEX_HOME"]) / "sy.config.toml" - plist = home / "Library" / "LaunchAgents" / "com.nvidia.switchyard.server.plist" - for path in (profile, plist): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("existing file\n") - before = { - path.relative_to(home): path.read_bytes() for path in home.rglob("*") if path.is_file() - } - command_log = home.parent / "commands.log" - env["SY_TEST_COMMAND_LOG"] = str(command_log) - for stub in Path(env["PATH"].split(":")[0]).iterdir(): - stub.write_text( - stub.read_text().replace( - "set -eu\n", 'set -eu\nprintf "%s\\n" "$0" >> "$SY_TEST_COMMAND_LOG"\n', 1 - ) - ) - - result = subprocess.run( - ["bash", str(scripts / script), *args], - env=env, - capture_output=True, - text=True, - timeout=10, - ) - - assert result.returncode == 2 - assert "Usage:" in result.stderr - assert result.stdout == "" - assert not command_log.exists() - after = { - path.relative_to(home): path.read_bytes() for path in home.rglob("*") if path.is_file() - } - assert after == before