diff --git a/Cargo.lock b/Cargo.lock index 5edf1df1581..866d1760b90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,7 @@ dependencies = [ "bytestring", "derive_more 2.0.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "http 0.2.12", "httparse", @@ -155,7 +155,7 @@ dependencies = [ "cfg-if", "derive_more 2.0.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -416,7 +416,7 @@ dependencies = [ "clipboard-win", "image 0.25.8", "log", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-core-graphics", @@ -856,6 +856,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.0" @@ -1085,7 +1091,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.5", ] [[package]] @@ -1289,7 +1295,7 @@ dependencies = [ "libc", "open", "relative-path", - "reqwest", + "reqwest 0.12.24", "rmcp", "rpassword", "scap-screencapturekit", @@ -1316,7 +1322,7 @@ dependencies = [ name = "cap-api" version = "0.0.0" dependencies = [ - "reqwest", + "reqwest 0.12.24", "workspace-hack", ] @@ -1494,7 +1500,7 @@ name = "cap-cursor-info" version = "0.0.0" dependencies = [ "hex", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "serde", "sha2", @@ -1572,7 +1578,7 @@ dependencies = [ "nix 0.29.0", "notify-rust", "objc", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "opentelemetry", @@ -1584,7 +1590,7 @@ dependencies = [ "rand 0.8.5", "regex", "relative-path", - "reqwest", + "reqwest 0.12.24", "rodio", "sanitize-filename", "scap-direct3d", @@ -1955,7 +1961,7 @@ dependencies = [ "libc", "libproc", "objc", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "parking_lot", "pipewire", @@ -2037,7 +2043,7 @@ dependencies = [ "libloading 0.8.8", "log", "metal 0.31.0", - "objc2 0.6.2", + "objc2 0.6.5", "pollster", "pretty_assertions", "rayon", @@ -2485,7 +2491,7 @@ checksum = "afede46921767868c5c7f8f55202bdd8bec0bab6bc9605174200f45924f93c62" dependencies = [ "clipboard-win", "image 0.25.8", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "windows 0.59.0", @@ -3058,6 +3064,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -3070,12 +3089,12 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.106", + "ctor-proc-macro", + "dtor 0.3.0", ] [[package]] @@ -3084,9 +3103,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" dependencies = [ - "dtor", + "dtor 0.8.1", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "cursor-icon" version = "1.2.0" @@ -3189,6 +3214,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + [[package]] name = "debugid" version = "0.8.0" @@ -3417,7 +3453,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.1", "libc", - "objc2 0.6.2", + "objc2 0.6.5", ] [[package]] @@ -3481,6 +3517,21 @@ dependencies = [ "litrs", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.1", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -3574,12 +3625,27 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + [[package]] name = "dtor" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -3939,6 +4005,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font-types" version = "0.9.0" @@ -4505,7 +4577,7 @@ checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "once_cell", "serde", @@ -4730,7 +4802,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -4821,10 +4893,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "0.2.12" @@ -4996,9 +5078,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png 0.17.16", @@ -5345,16 +5427,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -5528,11 +5600,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -5651,10 +5724,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.11.4", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -5748,6 +5821,15 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.10" @@ -5979,7 +6061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" dependencies = [ "cc", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-foundation 0.3.1", "time", ] @@ -6037,9 +6119,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.1", + "web_atoms", ] [[package]] @@ -6345,23 +6438,23 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "once_cell", - "png 0.17.16", + "png 0.18.0", "serde", "thiserror 2.0.16", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6774,9 +6867,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -6807,7 +6900,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.1", "libc", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-cloud-kit 0.3.1", "objc2-core-data 0.3.1", "objc2-core-foundation", @@ -6826,7 +6919,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.1", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-avf-audio", "objc2-core-foundation", "objc2-core-graphics", @@ -6843,7 +6936,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfc1d11521c211a7ebe17739fc806719da41f56c6b3f949d9861b459188ce910" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.5", "objc2-foundation 0.3.1", ] @@ -6856,7 +6949,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-core-location", + "objc2-core-location 0.2.2", "objc2-foundation 0.2.2", ] @@ -6867,7 +6960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-foundation 0.3.1", ] @@ -6889,7 +6982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" dependencies = [ "dispatch2", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-audio-types", "objc2-core-foundation", ] @@ -6901,7 +6994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", + "objc2 0.6.5", ] [[package]] @@ -6923,7 +7016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-foundation 0.3.1", ] @@ -6935,7 +7028,7 @@ checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.5", ] [[package]] @@ -6946,7 +7039,7 @@ checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-foundation", "objc2-io-surface", ] @@ -6969,7 +7062,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.5", "objc2-foundation 0.3.1", ] @@ -6985,6 +7078,16 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-location" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-core-media" version = "0.3.1" @@ -6993,7 +7096,7 @@ checksum = "f0b7afa6822e2fa20dfc88d10186b2432bf8560b5ed73ec9d31efd78277bc878" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -7007,7 +7110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1989c3e76c7e978cab0ba9e6f4961cd00ed14ca21121444cc26877403bfb6303" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -7050,7 +7153,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.1", "libc", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -7071,17 +7174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" -dependencies = [ - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -7116,7 +7209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", ] @@ -7141,19 +7234,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-security" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-foundation", + "objc2-foundation 0.3.1", ] [[package]] @@ -7178,13 +7261,13 @@ dependencies = [ "objc2-cloud-kit 0.2.2", "objc2-core-data 0.2.2", "objc2-core-image 0.2.2", - "objc2-core-location", + "objc2-core-location 0.2.2", "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", - "objc2-user-notifications", + "objc2-user-notifications 0.2.2", ] [[package]] @@ -7194,9 +7277,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.2", + "block2 0.6.1", + "objc2 0.6.5", + "objc2-cloud-kit 0.3.1", + "objc2-core-data 0.3.1", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.1", + "objc2-core-location 0.3.1", "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", + "objc2-user-notifications 0.3.1", ] [[package]] @@ -7219,10 +7310,20 @@ dependencies = [ "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-core-location", + "objc2-core-location 0.2.2", "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-user-notifications" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-web-kit" version = "0.3.1" @@ -7231,12 +7332,10 @@ checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ "bitflags 2.13.1", "block2 0.6.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", - "objc2-javascript-core", - "objc2-security", ] [[package]] @@ -7411,7 +7510,7 @@ dependencies = [ "bytes", "http 1.3.1", "opentelemetry", - "reqwest", + "reqwest 0.12.24", ] [[package]] @@ -7426,7 +7525,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest", + "reqwest 0.12.24", "thiserror 2.0.16", "tracing", ] @@ -7574,7 +7673,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.5", "objc2-foundation 0.3.1", "objc2-osa-kit", "serde", @@ -7741,10 +7840,20 @@ 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.8.0" @@ -7765,6 +7874,16 @@ dependencies = [ "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.8.0" @@ -7795,6 +7914,16 @@ dependencies = [ "rand 0.8.5", ] +[[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.10.0" @@ -7811,12 +7940,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.106", @@ -7849,6 +7978,15 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -8757,11 +8895,45 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + [[package]] name = "resize" version = "0.8.8" @@ -8812,7 +8984,7 @@ dependencies = [ "gtk-sys", "js-sys", "log", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -9168,7 +9340,7 @@ dependencies = [ "clap", "futures", "inquire", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "scap-targets", @@ -9397,14 +9569,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", + "cssparser 0.29.6", "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser 0.36.0", + "derive_more 2.0.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash 2.1.1", + "servo_arc 0.4.3", "smallvec", ] @@ -9441,7 +9632,7 @@ checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030" dependencies = [ "httpdate", "native-tls", - "reqwest", + "reqwest 0.12.24", "sentry-actix", "sentry-anyhow", "sentry-backtrace", @@ -9759,6 +9950,15 @@ dependencies = [ "stable_deref_trait", ] +[[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 = "sha1" version = "0.10.6" @@ -10216,6 +10416,18 @@ dependencies = [ "serde", ] +[[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.5.4" @@ -10228,6 +10440,18 @@ dependencies = [ "quote", ] +[[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" @@ -10392,6 +10616,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -10498,33 +10733,35 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.3" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.13.1", "block2 0.6.1", "core-foundation 0.10.1", - "core-graphics 0.24.0", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni 0.21.1", - "lazy_static", "libc", "log", "ndk 0.9.0", - "ndk-context", "ndk-sys 0.6.0+11769913", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", + "objc2-ui-kit 0.3.1", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -10570,9 +10807,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.8.5" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "6fa5bacdb9bbad5954af3d1bd6cf6ae9192cab1b2e270f4a07f904610b9e85f4" dependencies = [ "anyhow", "bytes", @@ -10592,7 +10829,7 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "objc2-ui-kit 0.3.1", @@ -10600,7 +10837,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.5", "serde", "serde_json", "serde_repr", @@ -10616,7 +10853,6 @@ dependencies = [ "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -10625,9 +10861,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -10641,15 +10877,14 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.7", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", @@ -10674,9 +10909,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -10821,7 +11056,7 @@ dependencies = [ "data-url", "http 1.3.1", "regex", - "reqwest", + "reqwest 0.12.24", "schemars 0.8.22", "serde", "serde_json", @@ -11017,7 +11252,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.12.24", "semver", "serde", "serde_json", @@ -11050,16 +11285,16 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", "gtk", "http 1.3.1", "jni 0.21.1", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-ui-kit 0.3.1", "objc2-web-kit", "raw-window-handle", @@ -11075,17 +11310,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http 1.3.1", "jni 0.21.1", "log", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", "once_cell", "percent-encoding", "raw-window-handle", @@ -11130,24 +11364,26 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", "cargo_metadata", - "ctor 0.2.9", + "ctor 0.8.0", + "dom_query", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http 1.3.1", "infer", "json-patch", "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex", @@ -11159,7 +11395,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.16", - "toml 0.9.7", + "toml 1.1.0+spec-1.1.0", "url", "urlpattern", "uuid", @@ -11212,6 +11448,15 @@ dependencies = [ "utf-8", ] +[[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 = "termcolor" version = "1.4.1" @@ -11710,20 +11955,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.13.1", "bytes", "futures-util", "http 1.3.1", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -11855,24 +12100,24 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", "once_cell", - "png 0.17.16", + "png 0.18.0", "serde", "thiserror 2.0.16", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -12398,9 +12643,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -12409,38 +12654,22 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.53" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ - "cfg-if", "js-sys", - "once_cell", + "tokio", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12448,22 +12677,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 3.0.9", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -12481,6 +12710,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.11" @@ -12592,9 +12834,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.80" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -12610,11 +12852,23 @@ 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 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -12636,9 +12890,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -12938,7 +13192,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -13757,7 +14011,7 @@ dependencies = [ "num-complex", "num-integer", "num-rational", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "open", @@ -13772,7 +14026,7 @@ dependencies = [ "regex", "regex-automata", "regex-syntax", - "reqwest", + "reqwest 0.12.24", "rgb", "rustc-hash 1.1.0", "rustix 0.38.44", @@ -13823,27 +14077,26 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wry" -version = "0.53.3" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2 0.6.1", "cookie", "crossbeam-channel", "dirs 6.0.0", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http 1.3.1", "javascriptcore-rs", "jni 0.21.1", - "kuchikiki", "libc", "ndk 0.9.0", - "objc2 0.6.2", + "objc2 0.6.5", "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -14248,3 +14501,7 @@ dependencies = [ "syn 2.0.106", "winnow 1.0.3", ] + +[[patch.unused]] +name = "tao" +version = "0.34.3" diff --git a/Cargo.toml b/Cargo.toml index a65433c7cc6..912b13aa224 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ tokio = { version = "1.39.3", features = [ "rt-multi-thread", "time", ] } -tauri = { version = "2.5.0", features = ["specta"] } +tauri = { version = "2.11.5", features = ["specta"] } specta = { version = "=2.0.0-rc.20", features = [ "derive", "serde_json", diff --git a/apps/desktop-gpui/Cargo.lock b/apps/desktop-gpui/Cargo.lock index 949e7bbb572..869eb69b140 100644 --- a/apps/desktop-gpui/Cargo.lock +++ b/apps/desktop-gpui/Cargo.lock @@ -1184,7 +1184,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -1541,7 +1541,7 @@ dependencies = [ "anyhow", "ashpd 0.11.1", "base64 0.22.1", - "block2 0.5.1", + "block2 0.6.2", "bytes", "cap-audio", "cap-camera", @@ -1582,9 +1582,11 @@ dependencies = [ "libc", "lyon_tessellation", "minisign-verify", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "parakeet-rs", "raw-window-handle", "reqwest 0.12.28", @@ -1901,7 +1903,7 @@ dependencies = [ "kameo", "libc", "objc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "parking_lot", "pipewire", @@ -1959,7 +1961,7 @@ dependencies = [ "libloading 0.8.9", "log", "metal 0.31.0", - "objc2 0.6.4", + "objc2 0.6.5", "rayon", "reactive_graph", "resvg 0.45.1", @@ -2314,7 +2316,7 @@ checksum = "afede46921767868c5c7f8f55202bdd8bec0bab6bc9605174200f45924f93c62" dependencies = [ "clipboard-win", "image 0.25.10", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "windows 0.59.0", @@ -3356,7 +3358,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3374,7 +3376,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -3592,7 +3594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4428,7 +4430,7 @@ checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "once_cell", "thiserror 2.0.20", @@ -4726,7 +4728,7 @@ dependencies = [ "media", "metal 0.33.0", "objc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "objc2-user-notifications", @@ -4865,7 +4867,7 @@ dependencies = [ "windows 0.61.3", "windows-core 0.61.2", "windows-numerics 0.2.0", - "windows-registry 0.5.3", + "windows-registry", ] [[package]] @@ -5278,7 +5280,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.6.1", + "windows-registry", ] [[package]] @@ -6254,7 +6256,7 @@ checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" dependencies = [ "cc", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "time", "uuid", @@ -6567,15 +6569,15 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -6844,7 +6846,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7027,9 +7029,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -7060,7 +7062,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-cloud-kit", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -7081,7 +7083,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-avf-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -7101,7 +7103,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7112,7 +7114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7123,7 +7125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio-types", "objc2-core-foundation", ] @@ -7135,7 +7137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -7157,7 +7159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7169,7 +7171,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -7179,10 +7181,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "libc", + "objc2 0.6.5", "objc2-core-foundation", "objc2-io-surface", + "objc2-metal 0.3.2", ] [[package]] @@ -7203,7 +7208,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7213,7 +7218,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7225,7 +7230,7 @@ checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -7239,7 +7244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", ] @@ -7251,7 +7256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -7293,7 +7298,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -7303,7 +7308,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b0446e98cf4a784cc7a0177715ff317eeaa8463841c616cfc78aa4f953c4ea" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", ] @@ -7325,7 +7330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -7335,7 +7340,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd9fdde720df3da7046bb9097811000c1e7ab5cd579fa89d96b27d56781fb30" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio-types", "objc2-core-foundation", "objc2-core-media", @@ -7361,7 +7366,7 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7385,8 +7390,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "block2 0.6.2", + "libc", + "objc2 0.6.5", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", ] @@ -7399,7 +7408,7 @@ checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-cloud-kit", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -7420,7 +7429,7 @@ checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-location", "objc2-foundation 0.3.2", ] @@ -7646,15 +7655,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-float" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -7699,7 +7699,7 @@ dependencies = [ "android_system_properties", "log", "nix 0.31.3", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "objc2-ui-kit", "serde", @@ -8278,7 +8278,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_edit 0.20.7", + "toml_edit 0.20.2", ] [[package]] @@ -8502,7 +8502,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8745,7 +8745,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", @@ -9130,7 +9130,7 @@ dependencies = [ "dispatch2", "js-sys", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -9268,7 +9268,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9411,7 +9411,7 @@ version = "0.1.0" dependencies = [ "cidre", "futures", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "tracing", @@ -10672,7 +10672,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.57.0", + "windows 0.54.0", ] [[package]] @@ -10686,7 +10686,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.57.0", + "windows 0.54.0", ] [[package]] @@ -10854,10 +10854,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -11220,9 +11220,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.14.0", "toml_datetime 0.6.11", @@ -11430,7 +11430,7 @@ dependencies = [ "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -12427,7 +12427,7 @@ dependencies = [ "naga 25.0.1", "ndk-sys 0.5.0+25.2.9519653", "objc", - "ordered-float 4.6.0", + "ordered-float", "parking_lot", "portable-atomic", "profiling", @@ -12470,13 +12470,13 @@ dependencies = [ "log", "naga 29.0.4", "ndk-sys 0.6.0+11769913", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", "objc2-quartz-core 0.3.2", "once_cell", - "ordered-float 5.3.0", + "ordered-float", "parking_lot", "portable-atomic", "portable-atomic-util", @@ -12611,7 +12611,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -12639,16 +12639,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -12757,18 +12747,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" @@ -12866,17 +12844,6 @@ dependencies = [ "windows-threading 0.2.1", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -12910,17 +12877,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-interface" version = "0.58.0" @@ -12996,17 +12952,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -13511,7 +13456,7 @@ dependencies = [ "num-complex", "num-integer", "num-rational", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "open", diff --git a/apps/desktop-gpui/Cargo.toml b/apps/desktop-gpui/Cargo.toml index f496ed30e58..a713117fbee 100644 --- a/apps/desktop-gpui/Cargo.toml +++ b/apps/desktop-gpui/Cargo.toml @@ -62,7 +62,10 @@ cap-project = { path = "../../crates/project" } # a `RenderImage` (`src/editor_sidebar/cursor.rs`). cap-cursor-info = { path = "../../crates/cursor-info" } resvg = "0.45" -rodio = { version = "0.19.0", default-features = false, features = ["mp3", "vorbis"] } +rodio = { version = "0.19.0", default-features = false, features = [ + "mp3", + "vorbis", +] } # The editor stack. `cap-rendering` is not a new subtree -- cap-recording # already depends on it, so wgpu 25 and its vendored wgpu-hal have been built # by this workspace since the recording unit; naming it here only makes the @@ -100,7 +103,11 @@ ffmpeg = { package = "ffmpeg-next", git = "https://github.com/CapSoftware/rust-f # `webp` is the config sidebar's four background-source illustrations, which # the app ships as webp; features are additive, so enabling it here does not # move gpui's own pin. -image = { version = "0.25.1", default-features = false, features = ["jpeg", "png", "webp"] } +image = { version = "0.25.1", default-features = false, features = [ + "jpeg", + "png", + "webp", +] } jpeg-decoder = "0.3" smallvec = "1" # Grapheme boundaries for `ui::TextInput`: Backspace deletes a user-perceived @@ -128,7 +135,10 @@ sanitize-filename = "0.6.0" semver = "1" base64 = "0.22" minisign-verify = "0.2.4" -rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] } +rfd = { version = "0.15", default-features = false, features = [ + "xdg-portal", + "tokio", +] } # The screenshot export compositor (`src/screenshot_export.rs`): the canvas-2d # work `renderScreenshotExportCanvas` does in the webview -- shape rasterizing, # text, and the share fingerprint. All three are already in this lockfile @@ -173,7 +183,14 @@ tauri-winres = "0.3.5" # decoders, renderer and preview tasks are all tokio-spawned -- a current-thread # runtime would deadlock the moment the test thread blocks on a frame. Kept in # dev-dependencies so the shipped binary's tokio features are unchanged. -tokio = { version = "1", features = ["rt-multi-thread", "time", "test-util", "net", "io-util", "macros"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "time", + "test-util", + "net", + "io-util", + "macros", +] } # Caption oracles (`src/transcription/verify_tests.rs`): fixture projects and # audio markers for the fake transcriber. cap-verify = { path = "../../crates/verify" } @@ -206,15 +223,37 @@ x11rb = "0.13" [target.'cfg(target_os = "macos")'.dependencies] # Native window treatment (panel levels, Spaces) gpui does not expose. Same -# versions gpui's own tree compiles, so no second objc2 universe gets built. -objc2 = "0.5.2" +# direct 0.6 line used by this app's class definitions and AppKit bindings. +objc2 = { version = "0.6.5", features = ["exception"] } # `NSVisualEffectView` is the pre-macOS-26 window-material fallback. Features # only -- the versions stay pinned, or a second objc2 universe gets built. -objc2-app-kit = { version = "0.2.2", features = ["NSAppearance", "NSWindow", "NSView", "NSResponder", "NSGraphics", "NSPasteboard", "NSVisualEffectView", "NSWorkspace", "NSAccessibility"] } +objc2-app-kit = { version = "0.3.2", features = [ + "NSAppearance", + "NSWindow", + "NSView", + "NSResponder", + "NSGraphics", + "NSPasteboard", + "NSMenuItemBadge", + "NSVisualEffectView", + "NSWorkspace", + "NSAccessibility", +] } +objc2-quartz-core = { version = "0.3.2", features = [ + "CALayer", + "CATransaction", + "objc2-core-foundation", +] } +objc2-core-graphics = { version = "0.3.2", features = ["CGColor"] } # NSRect/NSPoint/NSSize for `setFrame:display:`. Already in the tree as # objc2-app-kit's own dependency, so this adds nothing to the build. -objc2-foundation = { version = "0.2.2", features = ["NSNotification", "NSOperation", "NSString", "block2"] } -block2 = "0.5.1" +objc2-foundation = { version = "0.3.2", features = [ + "NSNotification", + "NSOperation", + "NSString", + "block2", +] } +block2 = "0.6" raw-window-handle = "0.6" core-foundation = "0.10" core-video = { version = "0.5.2", features = ["metal"] } diff --git a/apps/desktop-gpui/README.md b/apps/desktop-gpui/README.md index e54d9bbb361..f29044473cf 100644 --- a/apps/desktop-gpui/README.md +++ b/apps/desktop-gpui/README.md @@ -636,8 +636,8 @@ Sizes are the Tauri app's, from `apps/desktop/src-tauri/src/windows.rs`. | Teleprompter | 560×320 | **Done, with deviations** — resizable to the 420×220 floor, level 101 on all Spaces, the `"teleprompter"` material at radius 22, traffic lights at (14, 11) — the Tauri `(14, 14)` is a `position_window_controls` inset, not a top-left, auto-scroll from the ported `teleprompter-utils` maths, the full footer and settings popover, native window opacity, the `teleprompter` store section, capture exclusion + content protection. The script editor is a real multi-line field and Mirror is inert — see below | | Editor | 1275×800 | **E1–E5 done — window, shell, playback, timeline, editing, config sidebar.** The real 1275×800 window with the traffic lights at (20, 20) — the Tauri `(20, 32)` is a `position_window_controls` inset, not a top-left, the header, the letterboxed player, the 260px timeline strip and the 416px config sidebar with its six-tab rail; a real project through `EditorInstance`; play/pause (button + Space), a 60fps live playhead and clock, real audio, click/drag seeking, end-of-media stop; the timeline at 1:1 — all nine track types from the project's own config, waveforms, the ruler's resolution ladder, minimap, edge fade, hover ghost, zoom (keys, buttons, slider, wheel, pinch) and pan; and **timeline editing**: selection (single, ⌘-multi, ⇧-range, ⌘A), trim, move, split (S + C, with snapping), zoom-segment create/resize/move, delete, undo/redo and the debounced write back to `project-config.json`. and **the config sidebar in full**: the six-tab rail, the scroll body, selection routing, all six tabs at 1:1, the shared colour-correction section on both its targets, and the eight per-segment panels with multi-select where the source has it. Crop and export are the remaining units | | Screenshot editor | 1240×800 (min 800×600) | **Done, with deviations** — the real window (native traffic lights, resizable to the 800×600 floor), one editor per bundle keyed like the video editor's registry, opened from capture, Recents, the settings Screenshots page, the tray's Previous and image import. The still renders through the same GPU path the Tauri instance uses (`FrameRenderer::render_immediate`, `preserve_screen_alpha`, no camera) on a config watch, painted with `paint_image` instead of shipped over the frame websocket; edits publish to the renderer live, ride the 1000ms debounced `project-config.json` write, and walk a real undo history (⌘Z / ⌘⇧Z / ⌘Y, a drag's sixty intermediate values collapsed into one entry). Styling: background source tabs (color swatches, gradient presets, wallpaper grid with theme tabs and cached thumbnails, image picker), hex fields, blur/padding/rounding/shadow sliders at the popovers' ranges plus the advanced-shadow collapsible, squircle/rounded corner style, border toggle + width/color/opacity, aspect-ratio menu. Annotations: the full engine — all seven tools with their single-key shortcuts, the frame-space geometry of `layout.ts`/`arrow.ts` verbatim, selection handles, the per-type config bar (stroke/fill swatches with the colour popover, width/opacity/mask-level/text-size sliders), the layers panel with drag reorder, blur/pixelate masks resampled live off the rendered frame, inline text editing, ⌘C/⌘V duplicate, delete. Crop dialog: the `editor_crop` engine wholesale over `original.png` (`screenshot_crop.rs`) — Size/Position number boxes, the round ratio button and its options menu (right-click opens the same one at the cursor) with the snap-to-ratio toggle persisted in the shared store, Full/Reset, arrow-key nudges, one history entry on Save, Escape/backdrop cancel. Export: Copy/Save/Share all bake the annotations in (`screenshot_export.rs` — the `renderScreenshotExportCanvas` pass: masks re-filtered at export scale from an unmodified copy, shapes via tiny-skia, text via cosmic-text at the preview's Helvetica baseline, the output expanded to the union of image and annotation boxes with the source's white-fill rules); Copy composites over white and lands PNG on the clipboard, Save writes PNG, Share fingerprints the config (`sha256` over recursively key-sorted JSON), short-circuits to the stored link on a hash match, and otherwise uploads JPEG-0.9 (PNG when transparency is actually needed) through create-or-get + presigned PUT — reusing `sharing.id` so the link survives edits — then persists `SharingMeta { id, link, content_hash }`, with Tauri's exact auth gates and toast ladder; reveal, delete behind a native confirm. Deviations: no OCR text-selection overlay (Vision OCR feeding an invisible DOM-style selectable text layer has no gpui equivalent yet; the layer is invisible in the Tauri editor too, so zero visual impact); the colour swatches are inert — no native colour panel is wired to this window, the hex fields are the input; the skeleton's Cap logo is drawn still (the app ships no bare glyph to spin, only the full lockup); the crop options menu has no separator row (`ui::Menu` has none, so its two groups sit adjacent); no `PendingScreenshots` in-memory hand-off (the PNG is on disk before the editor opens) | -| Tray (status item) | menu bar | **Done, with deviations** — a native `NSStatusItem` (no gpui API exists for one; built on AppKit with the color-panel channel discipline), menu byte-identical to `build_tray_menu`: Open Main Window, Record Display/Window/Area (screenshot-mode relabels), Select Mode ▸ with the ✓, Previous ▸ with 32px cover-cropped thumbnails and the 🎬⚡📷 prefixes, View all recordings / screenshots, Settings, version row, Quit Cap. The icon follows the mode and becomes the stop icon while recording, when clicking the item stops the capture (the menu is detached so the button's action can fire). Select Mode writes the same `recording_settings.mode` store key the Tauri app reads. Take a Screenshot captures the display under the cursor and Import Media… routes video/image imports; a Previous screenshot opens the screenshot editor. Deviations: Upload Logs renders disabled (no log-upload infrastructure yet) | -| App menu + shortcuts | menu bar | **Done** — the `build_macos_app_menu` structure (Cap / File / Edit / View / Window / Help) via `cx.set_menus`, with ⌘W closing the key window through each window's own close path (main hides, Tauri's `CloseRequested` arm transcribed: camera bubble closed, overlays closed, feeds released when idle), ⌘M / Zoom / ⌃⌘F on the key window, ⌘H / ⌥⌘H, ⌘Q, and Edit items dispatching the text-input actions. Dock-icon policy (Regular ↔ Accessory per `hideDockIcon` + visible windows) synced on every window show/hide seam | +| Tray (status item) | menu bar | **Done, with deviations** — a native `NSStatusItem` (no gpui API exists for one; built on AppKit with the color-panel channel discipline), menu in `build_menu`'s own grouping: Open Main Window; the current mode as the group label (an `NSMenu` section header reading e.g. `Studio` plus a palette item with the mode icons on macOS 14+ (the header follows the palette's highlight in italic), a ✓-ticked `Recording Mode ▸` submenu on older macOS and on Windows); Record Display/Window/Area + Take a Screenshot (screenshot-mode relabels); Previous ▸ (clock glyph) with 32px cover-cropped thumbnails and the 🎬⚡📷 prefixes, View all recordings / screenshots; Import Media…, Settings (⌘,), Upload Logs; Quit Cap with the version as a trailing section header on macOS and as a disabled row above Quit elsewhere. The four record rows, Previous ▸, Import Media… and Settings each carry an SF Symbol (`display`, `interface.window` (`macwindow` below macOS 26), `rectangle.dashed`, `camera.on.rectangle`, `clock.arrow.circlepath`, `square.and.arrow.down`, `gear`) drawn straight from the system catalog on macOS 11+ with no fallback — an unresolvable name leaves the row bare — while Windows draws the cached 32×32 raster of the same SVG their in-app buttons use (`ROW_ICON_SOURCES` / `svg_icon_png`); the macOS glyphs are template images the menu tints, and Previous's own rows keep their photo thumbnails; the chrome rows (Open Main Window, View all …, Upload Logs, Quit) stay bare. The icon follows the mode and becomes the stop icon while recording, when clicking the item stops the capture (the menu is detached so the button's action can fire). The mode picker writes the same `recording_settings.mode` store key the Tauri app reads. Take a Screenshot captures the display under the cursor and Import Media… routes video/image imports; a Previous screenshot opens the screenshot editor. Deviations: Upload Logs renders disabled (no log-upload infrastructure yet) | +| App menu + shortcuts | menu bar | **Done** — the `build_macos_app_menu` structure (Cap / File / Edit / View / Window / Help) via `cx.set_menus`, with ⌘W closing the key window through each window's own close path (main hides, Tauri's `CloseRequested` arm transcribed: camera bubble closed, overlays closed, feeds released when idle), ⌘M / Zoom / ⌃⌘F on the key window, ⌘H / ⌥⌘H, ⌘Q, and Edit items dispatching the text-input actions. The Cap menu carries About (AppKit's own `orderFrontStandardAboutPanel:`, patched in by row position, so there is no gpui About action), Check for Update and Preferences… with ⌘,. The update row resolves to three states — `Check for Update`, `Download update…` and `Restart to update…`, with `Checking for Update…` greyed out while a check runs — and is rewritten in place in the live `NSMenu` (`platform::set_update_menu_item`, by row position, `setBadge:` on macOS 14+) so an open menu survives a status change; the version, and the download percentage on macOS, ride the badge, and the menu bar is only reinstalled on the other platforms when the state itself changes. File carries Open File… (an existing recording or screenshot, routed through the tray's `open_previous_item`), Import Media… (the same picker the tray runs), the AppKit-managed Open Recent list — every editor open registers its bundle with `add_recent_document`, and a click comes back through `application:openURLs:` → `deeplink` → `open_editor` — and Close Window. Help carries the Changelog and the cap.so/help links. Dock-icon policy (Regular ↔ Accessory per `hideDockIcon` + visible windows) synced on every window show/hide seam | ## Recording diff --git a/apps/desktop-gpui/src/app_windows.rs b/apps/desktop-gpui/src/app_windows.rs index b5aebb1b21e..f9fb9a8e798 100644 --- a/apps/desktop-gpui/src/app_windows.rs +++ b/apps/desktop-gpui/src/app_windows.rs @@ -4708,6 +4708,8 @@ fn open_editor_window( return None; } + cx.add_recent_document(&project_path); + if let Some(window_id) = cx .global::() .editors @@ -6599,6 +6601,8 @@ pub fn open_screenshot_editor(path: PathBuf, cx: &mut App) { }; let key = editor_key(&bundle); + cx.add_recent_document(&bundle); + if let Some(handle) = cx .global::() .screenshot_editors diff --git a/apps/desktop-gpui/src/editor_window.rs b/apps/desktop-gpui/src/editor_window.rs index d19e1cd05c6..5acbe7e664e 100644 --- a/apps/desktop-gpui/src/editor_window.rs +++ b/apps/desktop-gpui/src/editor_window.rs @@ -4445,9 +4445,7 @@ impl EditorWindow { return; } let valid_target = self.lane_reorder_position_valid(current.source, x, y); - let target_index = valid_target - .then(|| self.lane_reorder_target_index(current.source, y)) - .unwrap_or(current.target_index); + let target_index = if valid_target { self.lane_reorder_target_index(current.source, y) } else { current.target_index }; if promote { self.history.pause(); } diff --git a/apps/desktop-gpui/src/main_window.rs b/apps/desktop-gpui/src/main_window.rs index a318d54a0f7..3953585cb4b 100644 --- a/apps/desktop-gpui/src/main_window.rs +++ b/apps/desktop-gpui/src/main_window.rs @@ -1062,7 +1062,7 @@ impl MainWindow { Self { theme, // `rawOptions.mode` -- the recording mode is a persisted setting, - // and the tray's Select Mode submenu writes the same key, so the + // and the tray's mode picker writes the same key, so the // window has to start where the store left it rather than at a // hardcoded Instant. mode: Mode::from_store(), @@ -1842,7 +1842,7 @@ impl MainWindow { // `RecordingSettingsStore::set_mode` plus the two tray refreshes it // triggers (`handle_mode_selection`). Every mode affordance -- the // pill, the info panel, the mode select window and the tray's own - // Select Mode submenu -- reaches this method, so this is the one place + // mode picker -- reaches this method, so this is the one place // the setting is written. // Written inline, like every other store write in this app (and like // `RecordingSettingsStore::set_mode` itself): a background write would diff --git a/apps/desktop-gpui/src/menus.rs b/apps/desktop-gpui/src/menus.rs index 8e399636917..93f5f6c2336 100644 --- a/apps/desktop-gpui/src/menus.rs +++ b/apps/desktop-gpui/src/menus.rs @@ -7,8 +7,9 @@ //! Tauri app decides between Regular and Accessory from exactly the set of //! windows ⌘W changes. //! -//! The menu itself is [`build_menus`], a byte-for-byte transcription of -//! `build_macos_app_menu` (`apps/desktop/src-tauri/src/lib.rs:479-566`). gpui +//! The menu itself is [`build_menus`], the GPUI model for the native menu bar. +//! macOS standard menu items are upgraded in `platform` after GPUI installs it. +//! gpui //! builds the real `NSMenu` from it and takes each item's key equivalent from //! the *keymap*, so an action with no `KeyBinding` renders without a shortcut //! and an action with no live handler renders disabled @@ -37,6 +38,68 @@ use crate::{ /// submenu to and what the About/Hide/Quit labels interpolate. pub const APP_NAME: &str = "Cap"; +struct MenuRegistration(UpdateRow); + +impl Global for MenuRegistration {} + +/// What the Check for Update row currently says. Download progress only moves +/// the badge, and only macOS applies it without rebuilding the menu bar, so a +/// percentage tick never touches the menu on the other platforms. +#[derive(Clone, PartialEq)] +struct UpdateRow { + title: &'static str, + enabled: bool, + badge: Option, +} + +const CHECK_TITLE: &str = "Check for Update"; +const CHECKING_TITLE: &str = "Checking for Update…"; +const DOWNLOAD_TITLE: &str = "Download update…"; +const RESTART_TITLE: &str = "Restart to update…"; + +impl UpdateRow { + fn of(status: &crate::updates::UpdateStatus) -> Self { + use crate::updates::UpdateStatus; + + let title = match status { + UpdateStatus::Idle => CHECK_TITLE, + UpdateStatus::Checking => CHECKING_TITLE, + UpdateStatus::Downloading { .. } | UpdateStatus::Installing { .. } => DOWNLOAD_TITLE, + UpdateStatus::Ready { .. } | UpdateStatus::Installed { .. } => RESTART_TITLE, + }; + Self { + title, + enabled: !matches!(status, UpdateStatus::Checking), + badge: update_menu_badge(status), + } + } +} + +#[cfg(target_os = "macos")] +fn update_menu_badge(status: &crate::updates::UpdateStatus) -> Option { + use crate::updates::UpdateStatus; + + match status { + UpdateStatus::Downloading { + version, + fraction: Some(fraction), + } => Some(format!( + "v{version} · {}%", + (fraction * 100.0).round() as u32 + )), + UpdateStatus::Downloading { version, .. } + | UpdateStatus::Ready { version } + | UpdateStatus::Installing { version } + | UpdateStatus::Installed { version, .. } => Some(format!("v{version}")), + UpdateStatus::Idle | UpdateStatus::Checking => None, + } +} + +#[cfg(not(target_os = "macos"))] +fn update_menu_badge(_: &crate::updates::UpdateStatus) -> Option { + None +} + /// `env!("CARGO_PKG_VERSION")`, exactly as `build_tray_menu` spells it -- this /// crate's version, which is the gpui app's own (0.1.x) rather than the /// shipping desktop app's. Noted as a deviation in the report: there is no @@ -50,12 +113,29 @@ actions!( [ /// `PredefinedMenuItem::about` -- `orderFrontStandardAboutPanel`. About, + /// Manually check for or install a Cap update. + CheckForUpdate, /// `PredefinedMenuItem::hide` -- ⌘H. HideSelf, /// `PredefinedMenuItem::hide_others` -- ⌥⌘H. HideOthers, /// The Cap menu's explicit Quit item -- ⌘Q. Quit, + /// Open General settings from the application menu. + Preferences, + /// Choose a Cap recording to open. + OpenFile, + /// Import a video or image into the library. + ImportMedia, + /// Open the Changelog from the Help menu. + Changelog, + HelpDashboard, + HelpDocs, + HelpFaq, + HelpSelfHosting, + HelpCenter, + HelpStatus, + HelpDiscord, /// `PredefinedMenuItem::close_window` -- ⌘W. Present twice in the /// Tauri menu (File and Window), one action either way. CloseWindow, @@ -79,6 +159,7 @@ pub fn init(cx: &mut App) { // `on_key_down` project-undo). A global binding here would shadow both. cx.bind_keys([ KeyBinding::new("cmd-q", Quit, None), + KeyBinding::new("cmd-,", Preferences, None), KeyBinding::new("cmd-h", HideSelf, None), KeyBinding::new("alt-cmd-h", HideOthers, None), KeyBinding::new("cmd-w", CloseWindow, None), @@ -86,16 +167,49 @@ pub fn init(cx: &mut App) { KeyBinding::new("ctrl-cmd-f", ToggleFullscreen, None), ]); - cx.on_action(|_: &About, cx: &mut App| { - // Ordering a window front re-enters gpui's window callbacks, so it runs - // from a task with no borrow held (`platform::place_overlay_panel`'s - // rule). - cx.spawn(async move |_| platform::show_about_panel(APP_NAME, app_version())) - .detach(); + cx.on_action(|_: &CheckForUpdate, cx: &mut App| { + crate::updates::check_manually(cx); }); cx.on_action(|_: &HideSelf, cx: &mut App| cx.hide()); cx.on_action(|_: &HideOthers, cx: &mut App| cx.hide_other_apps()); cx.on_action(|_: &Quit, cx: &mut App| quit(cx)); + cx.on_action(|_: &Preferences, cx: &mut App| { + cx.defer(|cx| app_windows::open_settings(crate::settings_window::Page::General, cx)); + }); + cx.on_action(|_: &OpenFile, cx: &mut App| { + let (files, directories) = if cfg!(target_os = "macos") { + (true, false) + } else { + (false, true) + }; + let paths = cx.prompt_for_paths(gpui::PathPromptOptions { + files, + directories, + multiple: false, + prompt: Some("Open Recording".into()), + }); + cx.spawn(async move |cx| { + if let Ok(Ok(Some(paths))) = paths.await + && let Some(path) = paths.into_iter().next() + { + cx.update(|cx| crate::tray::open_previous_item(path, cx)); + } + }) + .detach(); + }); + cx.on_action(|_: &ImportMedia, cx: &mut App| { + crate::import::pick_and_import_media(cx); + }); + cx.on_action(|_: &Changelog, cx: &mut App| { + app_windows::open_settings(crate::settings_window::Page::Changelog, cx) + }); + cx.on_action(|_: &HelpDashboard, cx: &mut App| cx.open_url("https://cap.so/dashboard")); + cx.on_action(|_: &HelpDocs, cx: &mut App| cx.open_url("https://cap.so/docs")); + cx.on_action(|_: &HelpFaq, cx: &mut App| cx.open_url("https://cap.so/faq")); + cx.on_action(|_: &HelpSelfHosting, cx: &mut App| cx.open_url("https://cap.so/self-hosting")); + cx.on_action(|_: &HelpCenter, cx: &mut App| cx.open_url("https://help.cap.so/")); + cx.on_action(|_: &HelpStatus, cx: &mut App| cx.open_url("https://cap.openstatus.dev/")); + cx.on_action(|_: &HelpDiscord, cx: &mut App| cx.open_url("https://cap.link/discord")); cx.on_action(|_: &CloseWindow, cx: &mut App| close_key_window(cx)); cx.on_action(|_: &Minimize, cx: &mut App| with_key_native(cx, platform::minimize_native)); cx.on_action(|_: &Zoom, cx: &mut App| with_key_native(cx, platform::zoom_native)); @@ -110,20 +224,46 @@ pub fn init(cx: &mut App) { with_key_native(cx, platform::toggle_fullscreen_native) }); - cx.set_menus(build_menus()); + cx.set_global(MenuRegistration(UpdateRow::of(&crate::updates::status(cx)))); + install_menus(cx); } -/// The menu bar model. -/// -/// One-to-one with `build_macos_app_menu`, including the empty Help submenu -/// (`Submenu::with_id_and_items(.., HELP_SUBMENU_ID, "Help", true, &[])`). -pub fn build_menus() -> Vec { +pub(crate) fn refresh_update_menu(cx: &mut App) { + if !cx.has_global::() { + return; + } + + let row = UpdateRow::of(&crate::updates::status(cx)); + if cx.global::().0 == row { + return; + } + cx.global_mut::().0 = row.clone(); + + #[cfg(target_os = "macos")] + platform::set_update_menu_item(row.title, row.badge.as_deref(), row.enabled); + #[cfg(not(target_os = "macos"))] + install_menus(cx); +} + +fn install_menus(cx: &mut App) { + let update_status = crate::updates::status(cx); + cx.set_menus(build_menus(&update_status)); + #[cfg(target_os = "macos")] + platform::patch_standard_application_menu(); +} + +/// The menu bar hierarchy and app-specific destinations. +fn build_menus(update_status: &crate::updates::UpdateStatus) -> Vec { + let update_row = UpdateRow::of(update_status); vec![ Menu { name: APP_NAME.into(), disabled: false, items: vec![ MenuItem::action(format!("About {APP_NAME}"), About), + MenuItem::action(update_row.title, CheckForUpdate).disabled(!update_row.enabled), + MenuItem::separator(), + MenuItem::action("Preferences…", Preferences), MenuItem::separator(), MenuItem::os_submenu("Services", SystemMenuType::Services), MenuItem::separator(), @@ -136,7 +276,12 @@ pub fn build_menus() -> Vec { Menu { name: "File".into(), disabled: false, - items: vec![MenuItem::action("Close Window", CloseWindow)], + items: vec![ + MenuItem::action("Open File…", OpenFile), + MenuItem::action("Import Media…", ImportMedia), + MenuItem::separator(), + MenuItem::action("Close Window", CloseWindow), + ], }, Menu { name: "Edit".into(), @@ -180,7 +325,17 @@ pub fn build_menus() -> Vec { Menu { name: "Help".into(), disabled: false, - items: vec![], + items: vec![ + MenuItem::action("Changelog", Changelog), + MenuItem::separator(), + MenuItem::action("Dashboard", HelpDashboard), + MenuItem::action("Documentation", HelpDocs), + MenuItem::action("FAQ", HelpFaq), + MenuItem::action("Self-hosting", HelpSelfHosting), + MenuItem::action("Help Center", HelpCenter), + MenuItem::action("System Status", HelpStatus), + MenuItem::action("Join Discord Community", HelpDiscord), + ], }, ] } @@ -880,8 +1035,7 @@ mod tests { assert!(gate.pending.is_none()); } - /// The menu bar, transcribed from `build_macos_app_menu`. Separators are - /// spelled `"-"` so the ordering is checked too. + /// Separators are spelled `"-"` so the ordering is checked too. fn describe(menu: &Menu) -> (String, Vec) { let items = menu .items @@ -897,8 +1051,17 @@ mod tests { } #[test] - fn menu_bar_matches_the_tauri_structure() { - let menus: Vec<_> = build_menus().iter().map(describe).collect(); + fn menu_bar_includes_native_and_cap_destinations() { + let menus: Vec<_> = build_menus(&crate::updates::UpdateStatus::Idle) + .iter() + .map(describe) + .collect(); + let expected_file_items = vec![ + "Open File…".into(), + "Import Media…".into(), + "-".into(), + "Close Window".into(), + ]; assert_eq!( menus, vec![ @@ -906,6 +1069,9 @@ mod tests { "Cap".to_string(), vec![ "About Cap".to_string(), + "Check for Update".into(), + "-".into(), + "Preferences…".into(), "-".into(), "Services".into(), "-".into(), @@ -915,7 +1081,7 @@ mod tests { "Quit Cap".into(), ] ), - ("File".into(), vec!["Close Window".into()]), + ("File".into(), expected_file_items,), ( "Edit".into(), vec![ @@ -938,11 +1104,117 @@ mod tests { "Close Window".into(), ] ), - ("Help".into(), vec![]), + ( + "Help".into(), + vec![ + "Changelog".into(), + "-".into(), + "Dashboard".into(), + "Documentation".into(), + "FAQ".into(), + "Self-hosting".into(), + "Help Center".into(), + "System Status".into(), + "Join Discord Community".into(), + ], + ), ] ); } + /// The row is the second item of the App menu (`menu_bar_...` pins that + /// order); it greys out for the duration of a check so a second click + /// cannot fire another one. + #[test] + fn the_update_row_greys_out_while_checking() { + fn row(status: &crate::updates::UpdateStatus) -> (String, bool) { + let menus = build_menus(status); + match &menus[0].items[1] { + MenuItem::Action { name, disabled, .. } => (name.to_string(), *disabled), + _ => panic!("the update row is an action"), + } + } + + assert_eq!( + row(&crate::updates::UpdateStatus::Idle), + ("Check for Update".to_string(), false) + ); + assert_eq!( + row(&crate::updates::UpdateStatus::Checking), + ("Checking for Update…".to_string(), true) + ); + } + + /// The version lives only in the badge, and a percentage tick changes + /// nothing else -- that is what keeps a download from reinstalling the + /// menu bar on every progress event. + #[test] + fn the_update_row_keeps_the_version_out_of_the_title() { + use crate::updates::UpdateStatus; + use semver::Version; + + let version = Version::parse("0.4.0").unwrap(); + let downloading = |fraction| UpdateStatus::Downloading { + version: version.clone(), + fraction, + }; + + let cold = UpdateRow::of(&downloading(None)); + let warm = UpdateRow::of(&downloading(Some(0.42))); + assert_eq!(cold.title, "Download update…"); + assert!(cold.enabled); + assert_eq!(warm.title, cold.title); + + let ready = UpdateRow::of(&UpdateStatus::Ready { + version: version.clone(), + }); + let installed = UpdateRow::of(&UpdateStatus::Installed { + version: version.clone(), + executable: std::path::PathBuf::from("/Applications/Cap.app"), + }); + assert_eq!(ready.title, "Restart to update…"); + assert_eq!(installed.title, ready.title); + assert_eq!(installed.badge, ready.badge); + + let installing = UpdateRow::of(&UpdateStatus::Installing { + version: version.clone(), + }); + assert_eq!(installing.title, cold.title); + } + + #[cfg(target_os = "macos")] + #[test] + fn the_update_badge_shows_the_version_and_live_progress() { + use crate::updates::UpdateStatus; + use semver::Version; + + let version = Version::parse("0.4.0").unwrap(); + let badge = |status| UpdateRow::of(&status).badge; + + assert_eq!( + badge(UpdateStatus::Downloading { + version: version.clone(), + fraction: None, + }), + Some("v0.4.0".to_string()) + ); + assert_eq!( + badge(UpdateStatus::Downloading { + version: version.clone(), + fraction: Some(0.42), + }), + Some("v0.4.0 · 42%".to_string()) + ); + assert_eq!( + badge(UpdateStatus::Ready { + version: version.clone(), + }), + Some("v0.4.0".to_string()) + ); + assert_eq!(badge(UpdateStatus::Idle), None); + assert_eq!(badge(UpdateStatus::Checking), None); + } + /// `sync_macos_dock_visibility`'s three branches. #[test] fn dock_policy_matches_the_tauri_rule() { diff --git a/apps/desktop-gpui/src/platform.rs b/apps/desktop-gpui/src/platform.rs index 7c05c4f6bba..8e39012a4fc 100644 --- a/apps/desktop-gpui/src/platform.rs +++ b/apps/desktop-gpui/src/platform.rs @@ -641,9 +641,13 @@ mod mac { } use gpui::Window; - use objc2::rc::Id; + use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; - use objc2_app_kit::{NSView, NSWindow, NSWindowCollectionBehavior}; + use objc2_app_kit::{ + NSApplication, NSDocumentController, NSMenu, NSMenuItem, NSView, NSWindow, + NSWindowCollectionBehavior, + }; + use objc2_foundation::{NSObjectNSKeyValueCoding, NSString}; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use super::{ForcedAppearance, MaterialKind, PanelBehavior}; @@ -651,6 +655,150 @@ mod mac { static NATIVE_QUIT_REQUESTS: std::sync::OnceLock = std::sync::OnceLock::new(); + /// Rows of `menus::build_menus`' Cap and File menus. Both menus are built + /// from that model in this order, so the positions are fixed. + const APP_MENU: isize = 0; + const ABOUT_ROW: isize = 0; + const UPDATE_ROW: isize = 1; + const PREFERENCES_ROW: isize = 3; + const FILE_MENU: isize = 1; + const OPEN_RECENT_ROW: isize = 1; + + pub fn patch_standard_application_menu() { + use objc2::{exception, msg_send, runtime::AnyObject, sel}; + use std::panic::AssertUnwindSafe; + + let Some(main_thread) = objc2_foundation::MainThreadMarker::new() else { + tracing::error!("Cannot patch the application menu off the main thread"); + return; + }; + let app = NSApplication::sharedApplication(main_thread); + let Some(main_menu) = app.mainMenu() else { + tracing::warn!("GPUI did not install an application menu"); + return; + }; + let Some(app_menu) = main_menu + .itemAtIndex(APP_MENU) + .and_then(|item| item.submenu()) + else { + return; + }; + // gpui's `validateMenuItem:` answers only "is this action available?", + // which is always yes for Cap's global listeners -- so AppKit's + // auto-enable pass overwrites `MenuItem::Action`'s `disabled` every + // time the menu opens. The model's enabled state has to be + // authoritative instead, as it already is for the tray menu and for + // muda. + app_menu.setAutoenablesItems(false); + + if let Some(about) = app_menu.itemAtIndex(ABOUT_ROW) { + unsafe { + about.setAction(Some(sel!(orderFrontStandardAboutPanel:))); + about.setTarget(None); + about.setAllowsAutomaticKeyEquivalentLocalization(true); + } + } + if let Some(preferences) = app_menu.itemAtIndex(PREFERENCES_ROW) { + preferences.setAllowsAutomaticKeyEquivalentLocalization(true); + } + if let Some(quit) = app_menu + .numberOfItems() + .checked_sub(1) + .and_then(|index| app_menu.itemAtIndex(index)) + { + unsafe { + quit.setAction(Some(sel!(terminate:))); + quit.setTarget(None); + quit.setAllowsAutomaticKeyEquivalentLocalization(true); + } + } + + let Some(file_menu) = main_menu + .itemAtIndex(FILE_MENU) + .and_then(|item| item.submenu()) + else { + return; + }; + let has_recent_menu = file_menu.itemAtIndex(OPEN_RECENT_ROW).is_some_and(|item| { + item.title() + .isEqualToString(&NSString::from_str("Open Recent")) + }); + if has_recent_menu { + return; + } + + let recent_menu = NSMenu::new(main_thread); + recent_menu.setTitle(&NSString::from_str("Open Recent")); + let name = NSString::from_str("NSRecentDocumentsMenu"); + let key = NSString::from_str("menuName"); + let registered = exception::catch(AssertUnwindSafe(|| { + unsafe { recent_menu.setValue_forKey(Some(&*name as &AnyObject), &key) }; + })); + if let Err(error) = registered { + tracing::warn!("Could not register Open Recent menu name: {error:?}"); + } + + let clear_item = NSMenuItem::new(main_thread); + clear_item.setTitle(&NSString::from_str("Clear Menu")); + unsafe { clear_item.setAction(Some(sel!(clearRecentDocuments:))) }; + recent_menu.addItem(&clear_item); + + let recent_item = NSMenuItem::new(main_thread); + recent_item.setTitle(&NSString::from_str("Open Recent")); + recent_item.setSubmenu(Some(&recent_menu)); + file_menu.insertItem_atIndex(&recent_item, OPEN_RECENT_ROW); + + let controller = NSDocumentController::sharedDocumentController(main_thread); + let can_install_menu: bool = + unsafe { msg_send![&*controller, respondsToSelector: sel!(_installOpenRecentMenus)] }; + if can_install_menu { + unsafe { + let _: () = msg_send![&*controller, _installOpenRecentMenus]; + } + } else { + tracing::warn!("NSDocumentController cannot install the Open Recent menu"); + } + } + + /// Rewrites the Check for Update row in place. A menu item edited this way + /// keeps whatever menu the user has open on screen; reinstalling the menu + /// bar for every download-percentage tick would close it. + pub fn set_update_menu_item(title: &str, badge: Option<&str>, enabled: bool) { + use objc2::runtime::AnyClass; + use objc2_app_kit::NSMenuItemBadge; + use objc2_foundation::MainThreadMarker; + + let Some(main_thread) = MainThreadMarker::new() else { + return; + }; + let Some(item) = update_menu_item(main_thread) else { + tracing::warn!("The update row is missing from the application menu"); + return; + }; + item.setTitle(&NSString::from_str(title)); + item.setEnabled(enabled); + + // `NSMenuItemBadge` and `setBadge:` are macOS 14+, and objc2's class + // lookup panics when the class is absent. + if AnyClass::get(c"NSMenuItemBadge").is_none() { + return; + } + let badge = badge.map(|text| { + NSMenuItemBadge::initWithString(main_thread.alloc(), &NSString::from_str(text)) + }); + item.setBadge(badge.as_deref()); + } + + fn update_menu_item( + main_thread: objc2_foundation::MainThreadMarker, + ) -> Option> { + NSApplication::sharedApplication(main_thread) + .mainMenu()? + .itemAtIndex(APP_MENU)? + .submenu()? + .itemAtIndex(UPDATE_ROW) + } + unsafe extern "C-unwind" fn application_should_terminate( _: &AnyObject, _: Sel, @@ -682,7 +830,7 @@ mod mac { .as_ref() .ok_or("Application delegate is unavailable")?; let delegate_class = delegate.class(); - if delegate_class.name() != "GPUIApplicationDelegate" { + if delegate_class.name() != c"GPUIApplicationDelegate" { return Err("Native Quit requires the owned GPUI application delegate".into()); } let selector = sel!(applicationShouldTerminate:); @@ -697,12 +845,12 @@ mod mac { unsafe extern "C-unwind" fn(&AnyObject, Sel, *mut AnyObject) -> usize, Imp, >(application_should_terminate); - if !objc2::runtime::Bool::from_raw(ffi::class_addMethod( + if !ffi::class_addMethod( delegate_class as *const _ as *mut _, - selector.as_ptr(), - Some(implementation), + selector, + implementation, c"Q@:@".as_ptr(), - )) + ) .as_bool() { return Err("Could not install the native Quit handler".into()); @@ -856,12 +1004,12 @@ mod mac { // `NSGlassEffectView` exists only on macOS 26+; its absence is what // sends `macos-window-material.ts` down the vibrancy branch. if material_override.as_deref() != Some("vibrancy") - && let Some(glass_class) = AnyClass::get("NSGlassEffectView") + && let Some(glass_class) = AnyClass::get(c"NSGlassEffectView") { unsafe { let glass: *mut AnyObject = msg_send![glass_class, alloc]; let glass: *mut AnyObject = msg_send![glass, initWithFrame: bounds]; - if let Some(glass) = Id::from_raw(glass) { + if let Some(glass) = Retained::from_raw(glass) { set_clip_radius(radius); let identifier = NSString::from_str(LIQUID_GLASS_IDENTIFIER); let _: () = msg_send![&*glass, setIdentifier: &*identifier]; @@ -878,7 +1026,7 @@ mod mac { let _: () = msg_send![ &*content_view, addSubview: &*glass, - positioned: NSWindowOrderingMode::NSWindowBelow, + positioned: NSWindowOrderingMode::Below, relativeTo: std::ptr::null_mut::(), ]; return Some(MaterialKind::LiquidGlass); @@ -897,12 +1045,12 @@ mod mac { vibrancy.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow); vibrancy.setState(NSVisualEffectState::FollowsWindowActiveState); vibrancy.setAutoresizingMask( - NSAutoresizingMaskOptions::NSViewWidthSizable - | NSAutoresizingMaskOptions::NSViewHeightSizable, + NSAutoresizingMaskOptions::ViewWidthSizable + | NSAutoresizingMaskOptions::ViewHeightSizable, ); content_view.addSubview_positioned_relativeTo( &vibrancy, - NSWindowOrderingMode::NSWindowBelow, + NSWindowOrderingMode::Below, None, ); } @@ -951,7 +1099,7 @@ mod mac { use objc2::ffi::class_addMethod; use objc2_foundation::NSRect; - unsafe extern "C" fn constrain_shim( + unsafe extern "C-unwind" fn constrain_shim( _this: *mut AnyObject, _sel: Sel, frame: NSRect, @@ -961,22 +1109,30 @@ mod mac { } for name in ["GPUIWindow", "GPUIPanel"] { - let Some(class) = objc2::runtime::AnyClass::get(name) else { + let Some(class) = objc2::runtime::AnyClass::get(match name { + "GPUIWindow" => c"GPUIWindow", + "GPUIPanel" => c"GPUIPanel", + _ => continue, + }) else { continue; }; let added = unsafe { class_addMethod( - (class as *const objc2::runtime::AnyClass as *mut objc2::ffi::objc_class) - .cast(), - objc2::sel!(constrainFrameRect:toScreen:).as_ptr(), - Some(std::mem::transmute::< - unsafe extern "C" fn(*mut AnyObject, Sel, NSRect, *mut AnyObject) -> NSRect, - unsafe extern "C" fn(), - >(constrain_shim)), + (class as *const objc2::runtime::AnyClass).cast_mut(), + objc2::sel!(constrainFrameRect:toScreen:), + std::mem::transmute::< + unsafe extern "C-unwind" fn( + *mut AnyObject, + Sel, + NSRect, + *mut AnyObject, + ) -> NSRect, + objc2::runtime::Imp, + >(constrain_shim), c"{CGRect={CGPoint=dd}{CGSize=dd}}@:{CGRect={CGPoint=dd}{CGSize=dd}}@".as_ptr(), ) }; - if objc2::runtime::Bool::from_raw(added).as_bool() { + if added.as_bool() { tracing::info!("installed frame-constraint shim on {name}"); } } @@ -1049,7 +1205,7 @@ mod mac { /// A retained `NSWindow` that can outlive a `&Window` borrow -- what /// [`place_overlay_panel`] operates on. - pub struct NativeWindow(Id); + pub struct NativeWindow(Retained); /// The retained `NSWindow` behind a gpui window, for AppKit calls that /// must run *outside* any gpui update (see [`place_overlay_panel`]). @@ -1306,7 +1462,7 @@ mod mac { } /// The `NSWindow` behind a gpui window. - pub fn ns_window(window: &Window) -> Option> { + pub fn ns_window(window: &Window) -> Option> { // Fully qualified: `Window` also has an inherent `window_handle()` // returning gpui's own `AnyWindowHandle`. let handle = HasWindowHandle::window_handle(window).ok()?; @@ -1331,7 +1487,7 @@ mod mac { ns_window.setLevel(behavior.level); tracing::debug!( requested = behavior.level, - actual = unsafe { ns_window.level() }, + actual = ns_window.level(), "panel level applied" ); if behavior.join_all_spaces { @@ -1415,7 +1571,7 @@ mod mac { pub fn set_window_click_through(native: &NativeWindow, click_through: bool) -> bool { native.0.setIgnoresMouseEvents(click_through); - unsafe { native.0.ignoresMouseEvents() == click_through } + native.0.ignoresMouseEvents() == click_through } /// `orderOut:` -- hide without closing, the way the Tauri main window @@ -1513,7 +1669,7 @@ mod mac { static SOURCE: OnceLock = OnceLock::new(); *SOURCE.get_or_init(|| { - let Some(mut builder) = ClassBuilder::new("CapFileDragSource", NSObject::class()) + let Some(mut builder) = ClassBuilder::new(c"CapFileDragSource", NSObject::class()) else { return 0; }; @@ -1529,7 +1685,7 @@ mod mac { ) -> usize, ); } - if let Some(protocol) = AnyProtocol::get("NSDraggingSource") { + if let Some(protocol) = AnyProtocol::get(c"NSDraggingSource") { builder.add_protocol(protocol); } let class = builder.register(); @@ -1676,7 +1832,7 @@ mod mac { use objc2::{class, msg_send}; use objc2_foundation::NSString; unsafe { - let mut image: Option> = None; + let mut image: Option> = None; let cap_app = NSString::from_str("/Applications/Cap.app"); let exists: bool = { @@ -1688,7 +1844,7 @@ mod mac { if !workspace.is_null() { let raw: *mut AnyObject = msg_send![workspace, iconForFile: &*cap_app]; // `iconForFile:` is autoreleased; retain for the setter. - image = Id::retain(raw); + image = Retained::retain(raw); if let Some(image) = &image { // The workspace icon's nominal size is small (32pt); // the reps go up to 1024. Ask for dock resolution. @@ -1709,7 +1865,7 @@ mod mac { if !data.is_null() { let alloc: *mut AnyObject = msg_send![class!(NSImage), alloc]; let raw: *mut AnyObject = msg_send![alloc, initWithData: data]; - image = Id::from_raw(raw); + image = Retained::from_raw(raw); } } @@ -1992,7 +2148,7 @@ mod mac { /// Tauri app calls on the recording controls panel. pub fn show_window_without_focus(window: &Window) { if let Some(ns_window) = ns_window(window) { - unsafe { ns_window.orderFrontRegardless() }; + ns_window.orderFrontRegardless(); rerun_occlusion_handler_after_show(&ns_window); } } @@ -2000,7 +2156,7 @@ mod mac { /// The CGWindowID, which is also what `scap_targets::WindowId` wraps on /// macOS -- used to exclude our own controls bar from the capture. pub fn window_number(window: &Window) -> Option { - ns_window(window).map(|w| unsafe { w.windowNumber() }) + ns_window(window).map(|w| w.windowNumber()) } // -- The open panel ------------------------------------------------------ @@ -2044,11 +2200,11 @@ mod mac { let _: () = msg_send![panel, setCanCreateDirectories: true]; let _: () = msg_send![panel, setNameFieldStringValue: &*NSString::from_str(suggested)]; if !extensions.is_empty() { - let types: Vec> = extensions + let types: Vec> = extensions .iter() .map(|extension| NSString::from_str(extension)) .collect(); - let types = NSArray::from_vec(types); + let types = NSArray::from_retained_slice(&types); let _: () = msg_send![panel, setAllowedFileTypes: &*types]; } let response: isize = msg_send![panel, runModal]; @@ -2116,7 +2272,7 @@ mod mac { let data = NSData::with_bytes(bytes); let image: *mut AnyObject = msg_send![class!(NSImage), alloc]; let image: *mut AnyObject = msg_send![image, initWithData: &*data]; - let image = Id::from_raw(image).ok_or("Failed to load image")?; + let image = Retained::from_raw(image).ok_or("Failed to load image")?; let objects: *mut AnyObject = msg_send![class!(NSArray), arrayWithObject: &*image]; let _: isize = msg_send![pasteboard, clearContents]; let ok: Bool = msg_send![pasteboard, writeObjects: objects]; @@ -2141,11 +2297,11 @@ mod mac { let _: () = msg_send![panel, setCanChooseDirectories: false]; let _: () = msg_send![panel, setAllowsMultipleSelection: false]; - let types: Vec> = extensions + let types: Vec> = extensions .iter() .map(|extension| NSString::from_str(extension)) .collect(); - let types = NSArray::from_vec(types); + let types = NSArray::from_retained_slice(&types); let _: () = msg_send![panel, setAllowedFileTypes: &*types]; // `NSModalResponseOK`. @@ -2186,7 +2342,7 @@ mod mac { cancel: &str, warning: bool, ) -> bool { - use objc2::{class, msg_send, msg_send_id}; + use objc2::{class, msg_send}; use objc2_foundation::NSString; /// `NSAlertStyle`: `Warning = 0`, `Informational = 1`. `NSUInteger`, @@ -2199,7 +2355,7 @@ mod mac { const NS_ALERT_FIRST_BUTTON_RETURN: isize = 1000; unsafe { - let alert: Id = msg_send_id![class!(NSAlert), new]; + let alert: Retained = msg_send![class!(NSAlert), new]; let _: () = msg_send![&*alert, setMessageText: &*NSString::from_str(title)]; let _: () = msg_send![&*alert, setInformativeText: &*NSString::from_str(message)]; let _: () = msg_send![ @@ -2222,13 +2378,13 @@ mod mac { } pub fn alert_dialog(title: &str, message: &str) { - use objc2::{class, msg_send, msg_send_id}; + use objc2::{class, msg_send}; use objc2_foundation::NSString; const NS_ALERT_STYLE_INFORMATIONAL: usize = 1; unsafe { - let alert: Id = msg_send_id![class!(NSAlert), new]; + let alert: Retained = msg_send![class!(NSAlert), new]; let _: () = msg_send![&*alert, setMessageText: &*NSString::from_str(title)]; let _: () = msg_send![&*alert, setInformativeText: &*NSString::from_str(message)]; let _: () = msg_send![&*alert, setAlertStyle: NS_ALERT_STYLE_INFORMATIONAL]; @@ -2485,48 +2641,6 @@ mod mac { } } - // -- The About panel ------------------------------------------------------ - - /// `PredefinedMenuItem::about(.., AboutMetadata { name, version, .. })`, - /// which on macOS is `orderFrontStandardAboutPanelWithOptions:`. - /// - /// The Tauri metadata also carries `copyright` and `publisher` from - /// `tauri.conf.json` -- neither key exists in that file, so both are `None` - /// there and there is nothing to pass here either. - /// - /// Spins no modal run loop of its own (the panel is an ordinary window), but - /// ordering a window front re-enters gpui's window callbacks, so it keeps - /// the [`place_overlay_panel`] rule: call it from a task, never inside an - /// update. - pub fn show_about_panel(name: &str, version: &str) { - use objc2::{class, msg_send}; - use objc2_foundation::{NSArray, NSString}; - - unsafe { - let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication]; - if app.is_null() { - return; - } - // `dictionaryWithObjects:forKeys:` rather than a typed - // `NSDictionary` constructor, for the reason - // [`desktop_picture_path`] gives: the typed binding needs another - // objc2-app-kit/foundation feature, and the rule here is that those - // versions stay pinned to gpui's. - let values = - NSArray::from_vec(vec![NSString::from_str(name), NSString::from_str(version)]); - let keys = NSArray::from_vec(vec![ - NSString::from_str("ApplicationName"), - NSString::from_str("ApplicationVersion"), - ]); - let options: *mut AnyObject = msg_send![ - class!(NSDictionary), - dictionaryWithObjects: &*values, - forKeys: &*keys, - ]; - let _: () = msg_send![app, orderFrontStandardAboutPanelWithOptions: options]; - } - } - /// One-line diagnostic of everything AppKit weighs into occlusion /// visibility -- how the macOS 26 display-link failure was diagnosed; keep /// it for the next platform mystery. @@ -2537,10 +2651,10 @@ mod mac { "occlusion_raw={:#x} visible={} on_active_space={} alpha={} level={} number={}", w.occlusionState().bits(), w.isVisible(), - unsafe { w.isOnActiveSpace() }, - unsafe { w.alphaValue() }, - unsafe { w.level() }, - unsafe { w.windowNumber() }, + w.isOnActiveSpace(), + w.alphaValue(), + w.level(), + w.windowNumber(), )) } } @@ -2840,7 +2954,6 @@ mod stub { pub fn activation_policy() -> isize { -1 } - pub fn show_about_panel(_name: &str, _version: &str) {} } #[cfg(not(any(target_os = "macos", target_os = "windows")))] diff --git a/apps/desktop-gpui/src/platform/macos_exception_guard.rs b/apps/desktop-gpui/src/platform/macos_exception_guard.rs index 0348df8652a..e3c1f875df5 100644 --- a/apps/desktop-gpui/src/platform/macos_exception_guard.rs +++ b/apps/desktop-gpui/src/platform/macos_exception_guard.rs @@ -16,7 +16,7 @@ use std::sync::OnceLock; use objc2::{ ffi, msg_send, - rc::Id, + rc::Retained, runtime::{AnyClass, AnyObject, Imp, Sel}, sel, }; @@ -46,9 +46,7 @@ impl GuardedMethod { if self.original.get().is_some() || class.verify_sel::<(), ()>(selector).is_err() { return false; } - let method = unsafe { - ffi::class_getInstanceMethod((class as *const AnyClass).cast(), selector.as_ptr()) - }; + let method = unsafe { ffi::class_getInstanceMethod(class, selector) }; if method.is_null() { return false; } @@ -63,7 +61,7 @@ impl GuardedMethod { if self.original.set(original).is_err() { return false; } - unsafe { ffi::method_setImplementation(method, Some(guard_imp)) }; + let _ = unsafe { ffi::method_setImplementation(method, guard_imp) }; true } @@ -72,12 +70,12 @@ impl GuardedMethod { return false; }; let exception = unsafe { cap_call_void_imp_catching(original, receiver, selector) }; - let Some(exception) = (unsafe { Id::from_raw(exception) }) else { + let Some(exception) = (unsafe { Retained::from_raw(exception) }) else { return false; }; tracing::warn!( exception = %describe(&exception), - selector = %selector.name(), + selector = %selector.name().to_string_lossy(), "ignored an AppKit exception that would have crashed the app" ); true @@ -110,7 +108,7 @@ unsafe extern "C" fn touch_bar_finder_invalidate(receiver: *mut AnyObject, selec /// Call once on the main thread, before the app's run loop starts. pub(super) fn install() { - let Some(class) = AnyClass::get("_NSTouchBarFinderObservation") else { + let Some(class) = AnyClass::get(c"_NSTouchBarFinderObservation") else { tracing::info!("AppKit has no Touch Bar finder observation class; nothing to guard"); return; }; @@ -124,7 +122,7 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; use objc2::{ - ClassType, msg_send, msg_send_id, + ClassType, msg_send, runtime::{ClassBuilder, NSObject}, }; @@ -145,7 +143,7 @@ mod tests { static CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); CLASS.get_or_init(|| { let mut builder = - ClassBuilder::new("CapExceptionGuardTestObservation", NSObject::class()).unwrap(); + ClassBuilder::new(c"CapExceptionGuardTestObservation", NSObject::class()).unwrap(); unsafe { builder.add_method( sel!(invalidate), @@ -170,7 +168,7 @@ mod tests { sel!(invalidate), ) }; - let exception = unsafe { Id::from_raw(exception) } + let exception = unsafe { Retained::from_raw(exception) } .expect("removing an unregistered observer must throw"); let description = describe(&exception); assert!( @@ -186,7 +184,7 @@ mod tests { assert!(TEST_GUARD.install(class, sel!(invalidate), guarded)); assert!(!TEST_GUARD.install(class, sel!(invalidate), guarded)); - let instance: Id = unsafe { msg_send_id![class, new] }; + let instance: Retained = unsafe { msg_send![class, new] }; for _ in 0..3 { let before = removal_calls(); let _: () = unsafe { msg_send![&instance, invalidate] }; @@ -197,10 +195,10 @@ mod tests { #[test] fn appkit_still_has_the_touch_bar_finder_method_this_guards() { assert!( - AnyClass::get("NSApplication").is_some(), + AnyClass::get(c"NSApplication").is_some(), "AppKit is not loaded" ); - let class = AnyClass::get("_NSTouchBarFinderObservation") + let class = AnyClass::get(c"_NSTouchBarFinderObservation") .expect("AppKit renamed _NSTouchBarFinderObservation; re-check the guard"); assert!(class.verify_sel::<(), ()>(sel!(invalidate)).is_ok()); } diff --git a/apps/desktop-gpui/src/platform/macos_occlusion.rs b/apps/desktop-gpui/src/platform/macos_occlusion.rs index 0c440597919..6e6dc8fffba 100644 --- a/apps/desktop-gpui/src/platform/macos_occlusion.rs +++ b/apps/desktop-gpui/src/platform/macos_occlusion.rs @@ -7,7 +7,7 @@ use objc2::{ sel, }; -type OcclusionStateFn = unsafe extern "C" fn(*mut AnyObject, Sel) -> usize; +type OcclusionStateFn = unsafe extern "C-unwind" fn(*mut AnyObject, Sel) -> usize; struct OcclusionShim { original: OnceLock, @@ -46,15 +46,12 @@ impl OcclusionShim { let added = unsafe { class_addMethod( (class as *const AnyClass).cast_mut().cast(), - selector.as_ptr(), - Some(std::mem::transmute::< - OcclusionStateFn, - unsafe extern "C" fn(), - >(shim)), + selector, + std::mem::transmute::(shim), c"Q@:".as_ptr(), ) }; - Ok(Bool::from_raw(added).as_bool()) + Ok(added.as_bool()) } unsafe fn state(&self, receiver: *mut AnyObject, selector: Sel) -> usize { @@ -121,23 +118,29 @@ unsafe fn is_visible(receiver: *mut AnyObject) -> bool { static WINDOW_SHIM: OcclusionShim = OcclusionShim::new(); static PANEL_SHIM: OcclusionShim = OcclusionShim::new(); -unsafe extern "C" fn window_occlusion_state(receiver: *mut AnyObject, selector: Sel) -> usize { +unsafe extern "C-unwind" fn window_occlusion_state( + receiver: *mut AnyObject, + selector: Sel, +) -> usize { unsafe { WINDOW_SHIM.state(receiver, selector) } } -unsafe extern "C" fn panel_occlusion_state(receiver: *mut AnyObject, selector: Sel) -> usize { +unsafe extern "C-unwind" fn panel_occlusion_state( + receiver: *mut AnyObject, + selector: Sel, +) -> usize { unsafe { PANEL_SHIM.state(receiver, selector) } } pub(super) fn install() { for (name, state, implementation) in [ ( - "GPUIWindow", + c"GPUIWindow", &WINDOW_SHIM, window_occlusion_state as OcclusionStateFn, ), ( - "GPUIPanel", + c"GPUIPanel", &PANEL_SHIM, panel_occlusion_state as OcclusionStateFn, ), @@ -146,20 +149,27 @@ pub(super) fn install() { continue; }; match state.install(class, implementation) { - Ok(true) => tracing::info!("installed macOS occlusion shim on {name}"), + Ok(true) => { + tracing::info!(name = %name.to_string_lossy(), "installed macOS occlusion shim") + } Ok(false) => {} - Err(error) => tracing::warn!(%error, name, "could not install macOS occlusion shim"), + Err(error) => { + tracing::warn!(%error, name = %name.to_string_lossy(), "could not install macOS occlusion shim") + } } } } #[cfg(test)] mod tests { - use std::cell::Cell; + use std::{ + cell::Cell, + ffi::{CStr, CString}, + }; use objc2::{ - ClassType, msg_send_id, - rc::Id, + ClassType, msg_send, + rc::Retained, runtime::{ClassBuilder, NSObject}, }; @@ -181,37 +191,49 @@ mod tests { overridden_panel: &'static AnyClass, } - unsafe extern "C" fn native_window_state(_receiver: *mut AnyObject, _selector: Sel) -> usize { + unsafe extern "C-unwind" fn native_window_state( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> usize { WINDOW_CALLS.set(WINDOW_CALLS.get() + 1); RAW_STATE.get() } - unsafe extern "C" fn native_is_visible(_receiver: *mut AnyObject, _selector: Sel) -> Bool { + unsafe extern "C-unwind" fn native_is_visible( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> Bool { Bool::new(VISIBLE.get()) } - unsafe extern "C" fn native_panel_state(_receiver: *mut AnyObject, _selector: Sel) -> usize { + unsafe extern "C-unwind" fn native_panel_state( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> usize { PANEL_CALLS.set(PANEL_CALLS.get() + 1); RAW_STATE.get() } - unsafe extern "C" fn overriding_window_state( + unsafe extern "C-unwind" fn overriding_window_state( receiver: *mut AnyObject, _selector: Sel, ) -> usize { unsafe { msg_send![super(receiver, classes().window), occlusionState] } } - unsafe extern "C" fn overriding_panel_state(receiver: *mut AnyObject, _selector: Sel) -> usize { + unsafe extern "C-unwind" fn overriding_panel_state( + receiver: *mut AnyObject, + _selector: Sel, + ) -> usize { unsafe { msg_send![super(receiver, classes().panel), occlusionState] } } - fn inherit(name: &str, superclass: &AnyClass) -> &'static AnyClass { + fn inherit(name: &CStr, superclass: &AnyClass) -> &'static AnyClass { ClassBuilder::new(name, superclass).unwrap().register() } fn with_method( - name: &str, + name: &CStr, superclass: &AnyClass, implementation: OcclusionStateFn, ) -> &'static AnyClass { @@ -225,7 +247,7 @@ mod tests { CLASSES.get_or_init(|| { let native_window = { let mut builder = - ClassBuilder::new("CapOcclusionTestNativeWindow", NSObject::class()).unwrap(); + ClassBuilder::new(c"CapOcclusionTestNativeWindow", NSObject::class()).unwrap(); unsafe { builder.add_method( sel!(occlusionState), @@ -233,40 +255,38 @@ mod tests { ); builder.add_method( sel!(isVisible), - native_is_visible as unsafe extern "C" fn(_, _) -> _, + native_is_visible as unsafe extern "C-unwind" fn(_, _) -> _, ); } builder.register() }; let native_panel = with_method( - "CapOcclusionTestNativePanel", + c"CapOcclusionTestNativePanel", native_window, native_panel_state, ); - let window = inherit("CapOcclusionTestWindow", native_window); - let panel = inherit("CapOcclusionTestPanel", native_panel); + let window = inherit(c"CapOcclusionTestWindow", native_window); + let panel = inherit(c"CapOcclusionTestPanel", native_panel); assert!(WINDOW_SHIM.install(window, window_occlusion_state).unwrap()); assert!(PANEL_SHIM.install(panel, panel_occlusion_state).unwrap()); let mut inherited_window = window; let mut inherited_panel = panel; for depth in 0..4 { - inherited_window = inherit( - &format!("CapOcclusionTestWindowSubclass{depth}"), - inherited_window, - ); - inherited_panel = inherit( - &format!("CapOcclusionTestPanelSubclass{depth}"), - inherited_panel, - ); + let window_name = + CString::new(format!("CapOcclusionTestWindowSubclass{depth}")).unwrap(); + inherited_window = inherit(&window_name, inherited_window); + let panel_name = + CString::new(format!("CapOcclusionTestPanelSubclass{depth}")).unwrap(); + inherited_panel = inherit(&panel_name, inherited_panel); } let window_override = with_method( - "CapOcclusionTestWindowOverride", + c"CapOcclusionTestWindowOverride", window, overriding_window_state, ); let panel_override = with_method( - "CapOcclusionTestPanelOverride", + c"CapOcclusionTestPanelOverride", panel, overriding_panel_state, ); @@ -276,16 +296,19 @@ mod tests { inherited_window, inherited_panel, overridden_window: inherit( - "CapOcclusionTestInheritedWindowOverride", + c"CapOcclusionTestInheritedWindowOverride", window_override, ), - overridden_panel: inherit("CapOcclusionTestInheritedPanelOverride", panel_override), + overridden_panel: inherit( + c"CapOcclusionTestInheritedPanelOverride", + panel_override, + ), } }) } fn verify_states(class: &AnyClass, expected_window_calls: usize, expected_panel_calls: usize) { - let instance: Id = unsafe { msg_send_id![class, new] }; + let instance: Retained = unsafe { msg_send![class, new] }; for visible in [true, false] { VISIBLE.set(visible); for raw in [0, 0x2, 0x2000, 0x2002] { @@ -372,7 +395,7 @@ mod tests { #[test] fn promoting_a_window_switches_to_the_panel_implementation() { let classes = classes(); - let instance: Id = unsafe { msg_send_id![classes.window, new] }; + let instance: Retained = unsafe { msg_send![classes.window, new] }; RAW_STATE.set(0x2000); WINDOW_CALLS.set(0); PANEL_CALLS.set(0); @@ -392,17 +415,20 @@ mod tests { #[test] fn incompatible_native_method_does_not_install_a_shim() { - unsafe extern "C" fn wrong_signature(_receiver: *mut AnyObject, _selector: Sel) -> f64 { + unsafe extern "C-unwind" fn wrong_signature( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> f64 { 0.0 } let state = OcclusionShim::new(); let mut builder = - ClassBuilder::new("CapOcclusionTestWrongSignature", NSObject::class()).unwrap(); + ClassBuilder::new(c"CapOcclusionTestWrongSignature", NSObject::class()).unwrap(); unsafe { builder.add_method( sel!(occlusionState), - wrong_signature as unsafe extern "C" fn(_, _) -> _, + wrong_signature as unsafe extern "C-unwind" fn(_, _) -> _, ); } let class = builder.register(); @@ -414,12 +440,12 @@ mod tests { fn existing_class_override_is_not_replaced() { let state = OcclusionShim::new(); let class = with_method( - "CapOcclusionTestExistingOverride", + c"CapOcclusionTestExistingOverride", NSObject::class(), native_window_state, ); assert!(!state.install(class, window_occlusion_state).unwrap()); - let instance: Id = unsafe { msg_send_id![class, new] }; + let instance: Retained = unsafe { msg_send![class, new] }; RAW_STATE.set(0x2000); let raw: usize = unsafe { msg_send![&*instance, occlusionState] }; assert_eq!(raw, 0x2000); @@ -428,7 +454,7 @@ mod tests { #[test] fn missing_native_method_does_not_install_a_shim() { let state = OcclusionShim::new(); - let class = inherit("CapOcclusionTestMissingMethod", NSObject::class()); + let class = inherit(c"CapOcclusionTestMissingMethod", NSObject::class()); assert!(state.install(class, window_occlusion_state).is_err()); assert!(state.original.get().is_none()); assert!(class.instance_method(sel!(occlusionState)).is_none()); diff --git a/apps/desktop-gpui/src/platform/windows.rs b/apps/desktop-gpui/src/platform/windows.rs index 60ef53662d6..e4a76274900 100644 --- a/apps/desktop-gpui/src/platform/windows.rs +++ b/apps/desktop-gpui/src/platform/windows.rs @@ -671,8 +671,6 @@ pub fn activation_policy() -> isize { -1 } -pub fn show_about_panel(_name: &str, _version: &str) {} - pub fn escape_hotkey_events() -> flume::Receiver<()> { flume::unbounded().1 } diff --git a/apps/desktop-gpui/src/settings_pages.rs b/apps/desktop-gpui/src/settings_pages.rs index c0d5d6bb421..7ffc0113a9f 100644 --- a/apps/desktop-gpui/src/settings_pages.rs +++ b/apps/desktop-gpui/src/settings_pages.rs @@ -20,12 +20,12 @@ use std::{ time::Duration, }; +use cap_cli_install as cli_install; use gpui::{ AppContext as _, Context, Entity, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px, rgb, svg, }; -use cap_cli_install as cli_install; use serde::Deserialize; use serde_json::{Map, Value, json}; diff --git a/apps/desktop-gpui/src/settings_window.rs b/apps/desktop-gpui/src/settings_window.rs index cd7dd5b65d4..0fdb185e2d6 100644 --- a/apps/desktop-gpui/src/settings_window.rs +++ b/apps/desktop-gpui/src/settings_window.rs @@ -1832,6 +1832,7 @@ impl SettingsWindow { let theme = self.theme; let (update_label, update_ready) = match crate::updates::status(cx) { crate::updates::UpdateStatus::Idle => ("Check for updates".to_string(), false), + crate::updates::UpdateStatus::Checking => ("Checking for updates…".to_string(), false), crate::updates::UpdateStatus::Downloading { version, fraction: Some(fraction), diff --git a/apps/desktop-gpui/src/snapshot.rs b/apps/desktop-gpui/src/snapshot.rs index ea53a80bfc3..791333fe3a5 100644 --- a/apps/desktop-gpui/src/snapshot.rs +++ b/apps/desktop-gpui/src/snapshot.rs @@ -38,10 +38,10 @@ pub fn arm_from_env(cx: &mut App) { } else if target == "main" { Some(cx.global::().main.into()) } else { - cx.global::() - .onboarding - .map(Into::into) - }; + cx.global::() + .onboarding + .map(Into::into) + }; let Some(handle) = handle else { tracing::warn!("snapshot: window handle missing"); return None; diff --git a/apps/desktop-gpui/src/store.rs b/apps/desktop-gpui/src/store.rs index 73e17c7df78..c6c3406e0e3 100644 --- a/apps/desktop-gpui/src/store.rs +++ b/apps/desktop-gpui/src/store.rs @@ -986,7 +986,7 @@ pub fn set_studio_sound_defaults(enabled: bool, isolation: cap_project::VoiceIso set_store_value(AUDIO_ENHANCEMENT, Value::Object(section)) } /// `RecordingSettingsStore::KEY` (`src-tauri/src/recording_settings.rs:37`) -- -/// the section the tray's Select Mode submenu reads and writes. +/// the section the tray's mode picker reads and writes. pub const RECORDING_SETTINGS: &str = "recording_settings"; #[derive(Clone, Copy, Debug, Default, PartialEq)] diff --git a/apps/desktop-gpui/src/tray.rs b/apps/desktop-gpui/src/tray.rs index 001a6d2a8cf..39c3acd040b 100644 --- a/apps/desktop-gpui/src/tray.rs +++ b/apps/desktop-gpui/src/tray.rs @@ -13,11 +13,15 @@ //! foreground task drains that channel and dispatches with a clean borrow. //! Same seam the editor's overlay panels use for AppKit callbacks. //! -//! Menu strings, order and separators are byte-identical to `build_tray_menu`. +//! Labels mirror `build_tray_menu`, except on macOS where the mode group +//! names the current mode and its section header follows the palette's +//! highlight in italic; the grouping is [`build_menu`]'s own. //! "Upload Logs" renders disabled because the log-upload path does not exist //! here. +use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::sync::LazyLock; use gpui::{App, Global}; @@ -80,20 +84,120 @@ pub enum TrayItem { #[derive(Clone, Debug, PartialEq)] pub enum Entry { Separator, + ModePicker { + mode: Mode, + }, Item { title: String, item: Option, enabled: bool, - /// A 32x32 PNG, already cover-cropped. `IconMenuItem`'s image. + /// The row's image: a cover-cropped 32x32 photo thumbnail for + /// Previous, a rasterised vector glyph anywhere else. `IconMenuItem`'s + /// image -- Windows draws it; macOS draws `row_symbol`'s SF Symbol + /// instead (photos aside). icon: Option>, }, Submenu { title: String, enabled: bool, items: Vec, + /// The header's glyph, rasterised at [`ROW_ICON_SIZE`] -- the raster + /// Windows draws; macOS draws `PREVIOUS_SYMBOL` instead. + icon: Option>, + }, + /// A non-interactive group label: an `NSMenu` section header on macOS 14+. + SectionHeader { + title: String, }, } +/// The glyphs the action rows carry, keyed for [`menu_icon`] -- the same files +/// their in-app counterparts use. macOS prefers `row_symbol` over these; +/// Windows draws them. +const ROW_ICON_SOURCES: &[(&str, &[u8])] = &[ + ( + "display", + include_bytes!("../assets/icons/screen.svg") as &[u8], + ), + ( + "window", + include_bytes!("../assets/icons/window.svg") as &[u8], + ), + ("area", include_bytes!("../assets/icons/area.svg") as &[u8]), + ( + "screenshot", + include_bytes!("../assets/icons/screenshot.svg") as &[u8], + ), + ( + "import", + include_bytes!("../assets/icons/import.svg") as &[u8], + ), + ( + "settings", + include_bytes!("../assets/icons/settings.svg") as &[u8], + ), + ( + "previous", + include_bytes!("../assets/icons/clock.svg") as &[u8], + ), +]; + +/// `Entry::Item`'s `icon` size, and the raster size of every row glyph. +const ROW_ICON_SIZE: u32 = 32; + +/// An SVG rendered to a `size`×`size` PNG, scaled to fit and centred -- the +/// conversion `cap_mode_icon_png` makes at 90 for the palette, shared with the +/// menu rows at [`ROW_ICON_SIZE`]. Only the alpha survives a template tint. +fn svg_icon_png(source: &[u8], size: u32) -> Option> { + let tree = resvg::usvg::Tree::from_data(source, &resvg::usvg::Options::default()).ok()?; + let mut pixmap = resvg::tiny_skia::Pixmap::new(size, size)?; + let bounds = tree.size(); + let scale = (size as f32 / bounds.width()).min(size as f32 / bounds.height()); + let transform = resvg::tiny_skia::Transform::from_translate( + (size as f32 - bounds.width() * scale) / 2.0, + (size as f32 - bounds.height() * scale) / 2.0, + ) + .pre_scale(scale, scale); + resvg::render(&tree, transform, &mut pixmap.as_mut()); + pixmap.encode_png().ok() +} + +/// A row glyph as a PNG, rasterised once: a menu rebuild is cheap, seven SVG +/// renders are not. +fn menu_icon(name: &str) -> Option> { + static ICONS: LazyLock>> = LazyLock::new(|| { + ROW_ICON_SOURCES + .iter() + .filter_map(|(name, svg)| Some((*name, svg_icon_png(svg, ROW_ICON_SIZE)?))) + .collect() + }); + ICONS.get(name).cloned() +} + +/// The SF Symbol the macOS menu draws for an action row, keyed by its action; +/// Windows renders [`Entry::Item`]'s raster instead. Rows outside this list +/// (the chrome rows, the mode rows of the pre-14 menu) stay bare on both. +#[cfg(target_os = "macos")] +fn row_symbol(item: &TrayItem) -> Option<&'static str> { + match item { + TrayItem::RecordDisplay => Some("display"), + TrayItem::RecordWindow => Some(if objc2::available!(macos = 26.0) { + "interface.window" + } else { + "macwindow" + }), + TrayItem::RecordArea => Some("rectangle.dashed"), + TrayItem::TakeScreenshot => Some("camera.on.rectangle"), + TrayItem::ImportMedia => Some("square.and.arrow.down"), + TrayItem::OpenSettings => Some("gear"), + _ => None, + } +} + +/// The Previous submenu's header glyph, macOS side. +#[cfg(target_os = "macos")] +const PREVIOUS_SYMBOL: &str = "clock.arrow.circlepath"; + impl Entry { fn item(title: impl Into, item: TrayItem) -> Self { Self::Item { @@ -104,6 +208,16 @@ impl Entry { } } + /// An enabled row carrying one of the [`ROW_ICON_SOURCES`] glyphs. + fn icon_item(title: impl Into, item: TrayItem, icon: &str) -> Self { + Self::Item { + title: title.into(), + item: Some(item), + enabled: true, + icon: menu_icon(icon), + } + } + /// An item with no handler: present, greyed out. `MenuItem::with_id(.., /// false, ..)` is how `build_tray_menu` spells the version row. fn disabled(title: impl Into) -> Self { @@ -243,12 +357,14 @@ fn previous_submenu(previous: &[PreviousItem]) -> Entry { return Entry::Submenu { title: "Previous".into(), enabled: false, + icon: menu_icon("previous"), items: vec![Entry::disabled("No recent items")], }; } Entry::Submenu { title: "Previous".into(), enabled: true, + icon: menu_icon("previous"), items: previous .iter() .map(|item| Entry::Item { @@ -265,6 +381,14 @@ fn previous_submenu(previous: &[PreviousItem]) -> Entry { } } +fn mode_name(mode: Mode) -> &'static str { + match mode { + Mode::Studio => "Studio", + Mode::Instant => "Instant", + Mode::Screenshot => "Screenshot", + } +} + /// `create_mode_submenu`: a `✓ ` prefix on the current mode and three spaces on /// the others, so the labels stay aligned without a real check mark. fn mode_submenu(mode: Mode) -> Entry { @@ -283,8 +407,9 @@ fn mode_submenu(mode: Mode) -> Entry { ) }; Entry::Submenu { - title: "Select Mode".into(), + title: "Recording Mode".into(), enabled: true, + icon: None, items: vec![ label(Mode::Studio, "Studio"), label(Mode::Instant, "Instant"), @@ -295,26 +420,59 @@ fn mode_submenu(mode: Mode) -> Entry { /// `build_tray_menu`, minus the onboarding-minimal branch. pub fn build_menu(mode: Mode, previous: &[PreviousItem], version: &str) -> Vec { - let mut entries = vec![Entry::item("Open Main Window", TrayItem::OpenCap)]; + let mut entries = Vec::new(); - if mode == Mode::Screenshot { - entries.push(Entry::item("Screenshot Display", TrayItem::RecordDisplay)); - entries.push(Entry::item("Screenshot Window", TrayItem::RecordWindow)); - entries.push(Entry::item("Screenshot Area", TrayItem::RecordArea)); + entries.push(Entry::item("Open Main Window", TrayItem::OpenCap)); + entries.push(Entry::Separator); + if cfg!(target_os = "macos") { + entries.push(Entry::ModePicker { mode }); + entries.push(Entry::Separator); } else { - entries.push(Entry::item("Record Display", TrayItem::RecordDisplay)); - entries.push(Entry::item("Record Window", TrayItem::RecordWindow)); - entries.push(Entry::item("Record Area", TrayItem::RecordArea)); - entries.push(Entry::item("Take a Screenshot", TrayItem::TakeScreenshot)); + entries.push(mode_submenu(mode)); + entries.push(Entry::Separator); } - entries.push(Entry::item("Import Media...", TrayItem::ImportMedia)); + if mode == Mode::Screenshot { + entries.push(Entry::icon_item( + "Screenshot Display", + TrayItem::RecordDisplay, + "display", + )); + entries.push(Entry::icon_item( + "Screenshot Window", + TrayItem::RecordWindow, + "window", + )); + entries.push(Entry::icon_item( + "Screenshot Area", + TrayItem::RecordArea, + "area", + )); + } else { + entries.push(Entry::icon_item( + "Record Display", + TrayItem::RecordDisplay, + "display", + )); + entries.push(Entry::icon_item( + "Record Window", + TrayItem::RecordWindow, + "window", + )); + entries.push(Entry::icon_item( + "Record Area", + TrayItem::RecordArea, + "area", + )); + entries.push(Entry::icon_item( + "Take a Screenshot", + TrayItem::TakeScreenshot, + "screenshot", + )); + } entries.push(Entry::Separator); - entries.push(mode_submenu(mode)); entries.push(previous_submenu(previous)); - entries.push(Entry::Separator); - entries.push(Entry::item( "View all recordings", TrayItem::ViewAllRecordings, @@ -323,14 +481,32 @@ pub fn build_menu(mode: Mode, previous: &[PreviousItem], version: &str) -> Vec String { fn walk(entries: &[Entry], depth: usize, out: &mut String) { for entry in entries { let pad = " ".repeat(depth); match entry { Entry::Separator => out.push_str(&format!("{pad}---\n")), + Entry::ModePicker { mode } => { + out.push_str(&format!("{pad}{}\n", mode_name(*mode))); + for name in ["Studio", "Instant", "Screenshot"] { + out.push_str(&format!("{}{name}\n", " ".repeat(depth + 1))); + } + } + Entry::SectionHeader { title } => { + out.push_str(&format!("{pad}{title} [section]\n")); + } Entry::Item { title, enabled, @@ -515,10 +701,12 @@ pub fn describe_menu(entries: &[Entry]) -> String { title, enabled, items, + icon, } => { out.push_str(&format!( - "{pad}{title}{}\n", - if *enabled { "" } else { " [disabled]" } + "{pad}{title}{}{}\n", + if *enabled { "" } else { " [disabled]" }, + if icon.is_some() { " [icon]" } else { "" } )); walk(items, depth + 1, out); } @@ -610,19 +798,30 @@ fn harness_item(spec: &str, cx: &App) -> Option { #[cfg(target_os = "macos")] pub use mac::*; +#[cfg(target_os = "macos")] +mod mac_mode_picker; + #[cfg(target_os = "macos")] mod mac { - use std::cell::RefCell; + use std::cell::{Cell, RefCell}; use gpui::{App, Global}; - use objc2::rc::Id; - use objc2::runtime::{AnyObject, Sel}; - use objc2::{ClassType, DeclaredClass, class, msg_send, msg_send_id, sel}; - use objc2_foundation::NSString; + use objc2::rc::Retained; + use objc2::runtime::{NSObjectProtocol, ProtocolObject, Sel}; + use objc2::{AnyThread, MainThreadOnly, define_class, msg_send, sel}; + use objc2_app_kit::{ + NSEventModifierFlags, NSImage, NSMenu, NSMenuDelegate, + NSMenuItem, NSStatusBar, NSStatusBarButton, NSStatusItem, + }; + use objc2_foundation::{ + MainThreadMarker, NSData, NSString, + }; + + use super::mac_mode_picker::{append_mode_picker, install_key_hook}; use super::{ - Entry, PreviousItem, STOP_ICON, TrayItem, current_menu_entries, handle_item, mode_icon, - scan_previous, stop_recording, + Entry, PREVIOUS_SYMBOL, PreviousItem, STOP_ICON, TrayItem, current_menu_entries, + handle_item, mode_icon, mode_submenu, row_symbol, scan_previous, stop_recording, }; use crate::main_window::Mode; @@ -633,49 +832,61 @@ mod mac { const ICON_HEIGHT: f64 = 18.0; /// Not a menu item: the status button itself, clicked mid-recording. const STOP_TAG: isize = -1; + const REBUILD_TAG: isize = -2; thread_local! { - /// The live sender, read by the ObjC action. `thread_local` for the - /// reason `COLOR_PANEL_TX` is: AppKit only calls the action on the main - /// thread, so it needs no lock. static TRAY_TX: RefCell>> = const { RefCell::new(None) }; + static MENU_OPEN: Cell = const { Cell::new(false) }; + static REBUILD_PENDING: Cell = const { Cell::new(false) }; } - objc2::declare_class!( - /// The receiver for every tray action. No ivars: the tag identifies the - /// item and the sender lives in the thread-local above. + define_class!( + // SAFETY: NSObject has no subclassing requirements; this class has no ivars or Drop. + #[unsafe(super(objc2::runtime::NSObject))] + #[name = "CapGpuiTrayTarget"] + #[thread_kind = MainThreadOnly] struct TrayTarget; - unsafe impl ClassType for TrayTarget { - type Super = objc2::runtime::NSObject; - type Mutability = objc2::mutability::InteriorMutable; - const NAME: &'static str = "CapGpuiTrayTarget"; - } - - impl DeclaredClass for TrayTarget {} - - unsafe impl TrayTarget { + impl TrayTarget { /// Fires inside AppKit's menu-tracking run loop. Touching gpui here /// is what "RefCell already borrowed" looks like, so it only posts /// the tag. - #[method(capTrayItem:)] - fn tray_item(&self, sender: *mut AnyObject) { - if sender.is_null() { - return; - } - let tag: isize = unsafe { msg_send![sender, tag] }; - post(tag); + #[unsafe(method(capTrayItem:))] + fn tray_item(&self, sender: &NSMenuItem) { + post(sender.tag()); + } + + #[unsafe(method(capTrayMode:))] + fn tray_mode(&self, sender: &NSMenuItem) { + post(sender.tag()); } /// The status button's action while a recording is running. - #[method(capTrayStop:)] - fn tray_stop(&self, _sender: *mut AnyObject) { + #[unsafe(method(capTrayStop:))] + fn tray_stop(&self, _sender: &NSStatusBarButton) { post(STOP_TAG); } } + + unsafe impl NSObjectProtocol for TrayTarget {} + + unsafe impl NSMenuDelegate for TrayTarget { + #[unsafe(method(menuWillOpen:))] + fn menu_will_open(&self, _menu: &NSMenu) { + MENU_OPEN.with(|o| o.set(true)); + } + + #[unsafe(method(menuDidClose:))] + fn menu_did_close(&self, _menu: &NSMenu) { + MENU_OPEN.with(|o| o.set(false)); + if REBUILD_PENDING.with(|p| p.replace(false)) { + post(REBUILD_TAG); // handled by the drain, with a clean borrow + } + } + } ); - fn post(tag: isize) { + pub(super) fn post(tag: isize) { TRAY_TX.with(|tx| { if let Some(sender) = tx.borrow().as_ref() { let _ = sender.send(tag); @@ -684,11 +895,11 @@ mod mac { } struct Tray { - status_item: Id, - target: Id, + status_item: Retained, + target: Retained, /// Retained so the menu can be detached (recording) and reattached /// (stopped) without rebuilding it. - menu: Option>, + menu: Option>, /// Tag -> item, rebuilt with the menu. The index *is* the tag. actions: Vec, mode: Mode, @@ -706,6 +917,7 @@ mod mac { return; }; cx.set_global(tray); + install_key_hook(); let (tx, rx) = flume::unbounded::(); TRAY_TX.with(|slot| *slot.borrow_mut() = Some(tx)); @@ -715,9 +927,16 @@ mod mac { cx.spawn(async move |cx| { while let Ok(tag) = rx.recv_async().await { cx.update(|cx| { - if tag == STOP_TAG { - stop_recording(cx); - return; + match tag { + STOP_TAG => { + stop_recording(cx); + return; + } + REBUILD_TAG => { + rebuild(cx); + return; + } + _ => {} } let item = cx.global::().actions.get(tag as usize).cloned(); match item { @@ -748,77 +967,62 @@ mod mac { } fn create_status_item() -> Option { - unsafe { - let status_bar: *mut AnyObject = msg_send![class!(NSStatusBar), systemStatusBar]; - if status_bar.is_null() { - return None; - } - let raw: *mut AnyObject = - msg_send![status_bar, statusItemWithLength: NS_VARIABLE_STATUS_ITEM_LENGTH]; - // `statusItemWithLength:` hands back an autoreleased object owned - // by the status bar; retain it for as long as we hold it, and never - // release it -- removing a status item is what makes it disappear - // from the menu bar, and this one lives for the process. - let status_item = Id::retain(raw)?; - let target: Id = msg_send_id![TrayTarget::alloc(), init]; - - let mode = Mode::from_store(); - let tray = Tray { - status_item, - target, - menu: None, - actions: Vec::new(), - mode, - previous: Vec::new(), - recording: false, - }; - set_button_image(&tray, mode_icon(mode)); - let button: *mut AnyObject = msg_send![&*tray.status_item, button]; - let visible: bool = msg_send![&*tray.status_item, isVisible]; - let frame: objc2_foundation::NSRect = if button.is_null() { + let mtm = MainThreadMarker::new()?; + let status_item = + NSStatusBar::systemStatusBar().statusItemWithLength(NS_VARIABLE_STATUS_ITEM_LENGTH); + let target: Retained = unsafe { msg_send![TrayTarget::alloc(mtm), init] }; + + let mode = Mode::from_store(); + let tray = Tray { + status_item, + target, + menu: None, + actions: Vec::new(), + mode, + previous: Vec::new(), + recording: false, + }; + set_button_image(&tray, mode_icon(mode)); + let button = tray.status_item.button(mtm); + let frame = button.as_ref().map_or_else( + || { objc2_foundation::NSRect::new( - objc2_foundation::NSPoint::new(0., 0.), - objc2_foundation::NSSize::new(0., 0.), + objc2_foundation::NSPoint::new(0.0, 0.0), + objc2_foundation::NSSize::new(0.0, 0.0), ) - } else { - msg_send![button, frame] - }; - tracing::info!( - has_button = !button.is_null(), - visible, - width = frame.size.width, - height = frame.size.height, - ?mode, - "status item created" - ); - Some(tray) - } + }, + |button| button.frame(), + ); + tracing::info!( + has_button = button.is_some(), + visible = tray.status_item.isVisible(), + width = frame.size.width, + height = frame.size.height, + ?mode, + "status item created" + ); + Some(tray) } /// `set_icon_for_ns_status_item_button`: an 18pt-tall template image on the /// status item's button. fn set_button_image(tray: &Tray, png: &[u8]) { - unsafe { - let button: *mut AnyObject = msg_send![&*tray.status_item, button]; - if button.is_null() { - return; - } - let Some(image) = ns_image(png, ICON_HEIGHT) else { - return; - }; - let _: () = msg_send![button, setImage: &*image]; - // `icon_as_template(cfg!(target_os = "macos"))` in `create_tray`. - let _: () = msg_send![&*image, setTemplate: true]; - // The verification seam. A machine with a full menu bar parks - // overflow status items behind the notch, where they cannot be - // photographed -- so which icon is on the button is logged, keyed - // by the asset's byte length (each of the four differs). - tracing::info!( - icon = icon_name(png), - bytes = png.len(), - "status item icon set" - ); - } + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let Some(button) = tray.status_item.button(mtm) else { + return; + }; + let Some(image) = ns_image(png, ICON_HEIGHT) else { + return; + }; + button.setImage(Some(&image)); + image.setTemplate(true); + tracing::info!( + icon = icon_name(png), + bytes = png.len(), + "status item icon set" + ); } fn icon_name(png: &[u8]) -> &'static str { @@ -839,35 +1043,42 @@ mod mac { /// An `NSImage` from PNG bytes, scaled to `height` points with its aspect /// preserved -- the conversion muda and tray-icon both do. - fn ns_image(png: &[u8], height: f64) -> Option> { - unsafe { - let data: *mut AnyObject = msg_send![ - class!(NSData), - dataWithBytes: png.as_ptr().cast::(), - length: png.len(), - ]; - if data.is_null() { - return None; - } - let alloc: *mut AnyObject = msg_send![class!(NSImage), alloc]; - let raw: *mut AnyObject = msg_send![alloc, initWithData: data]; - let image = Id::from_raw(raw)?; - let size: objc2_foundation::NSSize = msg_send![&*image, size]; - if size.height > 0.0 { - let width = size.width / (size.height / height); - let _: () = msg_send![ - &*image, - setSize: objc2_foundation::NSSize::new(width, height) - ]; - } - Some(image) + fn ns_image(bytes: &[u8], height: f64) -> Option> { + let data = NSData::with_bytes(bytes); + let image = NSImage::initWithData(NSImage::alloc(), &data)?; + let size = image.size(); + if size.height > 0.0 { + image.setSize(objc2_foundation::NSSize::new( + size.width / (size.height / height), + height, + )); } + Some(image) + } + + /// An SF Symbol at the size the system serves it (15-20pt, the range menu + /// icons live in) -- already a template image. The API only exists on + /// macOS 11+, and an unknown name comes back nil; both cases leave the + /// row bare rather than falling back to the raster, which Windows owns. + pub(super) fn symbol_image(name: &str) -> Option> { + if !objc2::available!(macos = 11.0) { + return None; + } + NSImage::imageWithSystemSymbolName_accessibilityDescription(&NSString::from_str(name), None) } /// Rebuild the menu from the current mode and Previous cache, and attach it /// (unless a recording is running, in which case the *button* owns the /// click -- see [`set_recording`]). pub fn rebuild(cx: &mut App) { + if MENU_OPEN.with(Cell::get) { + REBUILD_PENDING.with(|p| p.set(true)); + return; + } + if !cx.has_global::() { + return; + } + if !cx.has_global::() { return; } @@ -878,17 +1089,13 @@ mod mac { let entries = current_menu_entries(cx, mode, &previous); let mut actions = Vec::new(); - let menu = { - let tray = cx.global::(); - build_ns_menu(&entries, &tray.target, &mut actions) - }; + let tray = cx.global::(); + let menu = build_ns_menu(&entries, &tray.target, &mut actions); + menu.setDelegate(Some(ProtocolObject::from_ref(&*tray.target))); - let recording = cx.global::().recording; - unsafe { - if !recording { - let tray = cx.global::(); - let _: () = msg_send![&*tray.status_item, setMenu: &*menu]; - } + let tray = cx.global::(); + if !tray.recording { + tray.status_item.setMenu(Some(&menu)); } let tray = cx.global_mut::(); @@ -900,82 +1107,137 @@ mod mac { entries: &[Entry], target: &TrayTarget, actions: &mut Vec, - ) -> Id { - unsafe { - let alloc: *mut AnyObject = msg_send![class!(NSMenu), alloc]; - let raw: *mut AnyObject = msg_send![alloc, init]; - let menu = Id::from_raw(raw).expect("NSMenu init returned nil"); - // Without this AppKit decides an item's enabled state from whether - // its target responds to the action -- which ours always does, so - // the four deliberately-disabled rows would come back enabled. - let _: () = msg_send![&*menu, setAutoenablesItems: false]; - - for entry in entries { - let item = build_ns_item(entry, target, actions); - let _: () = msg_send![&*menu, addItem: &*item]; + ) -> Retained { + let mtm = + MainThreadMarker::new().expect("tray menu construction must run on the main thread"); + let menu = NSMenu::new(mtm); + menu.setAutoenablesItems(false); + + for entry in entries { + if let Entry::ModePicker { mode } = entry { + if objc2::available!(macos = 14.0) { + append_mode_picker(&menu, *mode, actions, mtm); + } else { + let submenu = mode_submenu(*mode); + menu.addItem(&build_ns_item(&submenu, target, actions, mtm)); + } + continue; } - menu + let item = build_ns_item(entry, target, actions, mtm); + menu.addItem(&item); } + + menu } fn build_ns_item( entry: &Entry, target: &TrayTarget, actions: &mut Vec, - ) -> Id { - unsafe { - match entry { - Entry::Separator => { - let raw: *mut AnyObject = msg_send![class!(NSMenuItem), separatorItem]; - Id::retain(raw).expect("separatorItem returned nil") + mtm: MainThreadMarker, + ) -> Retained { + match entry { + Entry::Separator => NSMenuItem::separatorItem(mtm), + Entry::ModePicker { mode } => { + let submenu = mode_submenu(*mode); + build_ns_item(&submenu, target, actions, mtm) + } + Entry::Item { + title, + item, + enabled, + icon, + } => { + let menu_item = new_menu_item(title, Some(sel!(capTrayItem:)), mtm); + unsafe { menu_item.setTarget(Some(target)) }; + menu_item.setEnabled(*enabled && item.is_some()); + if let Some(item) = item { + menu_item.setTag(actions.len() as isize); + actions.push(item.clone()); } - Entry::Item { - title, - item, - enabled, - icon, - } => { - let menu_item = new_menu_item(title, sel!(capTrayItem:)); - let _: () = msg_send![&*menu_item, setTarget: target]; - let _: () = msg_send![&*menu_item, setEnabled: *enabled && item.is_some()]; - if let Some(item) = item { - let tag = actions.len() as isize; - let _: () = msg_send![&*menu_item, setTag: tag]; - actions.push(item.clone()); + if *item == Some(TrayItem::OpenSettings) { + menu_item.setKeyEquivalent(&NSString::from_str(",")); + menu_item.setKeyEquivalentModifierMask(NSEventModifierFlags::Command); + } + // Previous's rows are photo thumbnails; every other iconed row + // is an SF Symbol, template-tinted by the menu. Windows draws + // `icon`'s raster instead. + let photo = matches!(item, Some(TrayItem::PreviousItem(_))); + let image = if photo { + let image = icon + .as_deref() + .and_then(|bytes| ns_image(bytes, ICON_HEIGHT)); + if image.is_none() { + tracing::warn!(%title, "no thumbnail for a previous row"); } - if let Some(icon) = icon - && let Some(image) = ns_image(icon, ICON_HEIGHT) - { - let _: () = msg_send![&*menu_item, setImage: &*image]; + image + } else if let Some(symbol) = item.as_ref().and_then(row_symbol) { + let image = symbol_image(symbol); + if image.is_none() { + tracing::warn!(%title, symbol, "SF Symbol not resolved"); + } + image + } else { + None + }; + + if let Some(image) = image { + image.setTemplate(!photo); + menu_item.setImage(Some(&image)); + + if objc2::available!(macos = 27.0) { + // NSMenuItemImageVisibility: automatic = 0, visible = 1, hidden = 2 + let _: () = + unsafe { msg_send![&*menu_item, setPreferredImageVisibility: 1isize] }; } - menu_item } - Entry::Submenu { - title, - enabled, - items, - } => { - let menu_item = new_menu_item(title, sel!(capTrayItem:)); - let submenu = build_ns_menu(items, target, actions); - let _: () = msg_send![&*menu_item, setSubmenu: &*submenu]; - let _: () = msg_send![&*menu_item, setEnabled: *enabled]; + menu_item + } + Entry::SectionHeader { title } => { + if objc2::available!(macos = 14.0) { + NSMenuItem::sectionHeaderWithTitle(&NSString::from_str(title), mtm) + } else { + let menu_item = NSMenuItem::new(mtm); + menu_item.setTitle(&NSString::from_str(title)); + menu_item.setEnabled(false); menu_item } } + Entry::Submenu { + title, + enabled, + items, + .. + } => { + let menu_item = new_menu_item(title, Some(sel!(capTrayItem:)), mtm); + unsafe { menu_item.setTarget(Some(target)) }; + let submenu = build_ns_menu(items, target, actions); + menu_item.setSubmenu(Some(&submenu)); + // Only Previous carries a header glyph; its own rows keep + // their photos, its header gets the symbol Windows draws the + // raster for. + if title == "Previous" + && let Some(image) = symbol_image(PREVIOUS_SYMBOL) + { + image.setTemplate(true); + menu_item.setImage(Some(&image)); + } + menu_item.setEnabled(*enabled); + menu_item + } } } - fn new_menu_item(title: &str, action: Sel) -> Id { - unsafe { - let alloc: *mut AnyObject = msg_send![class!(NSMenuItem), alloc]; - let raw: *mut AnyObject = msg_send![ - alloc, - initWithTitle: &*NSString::from_str(title), - action: action, - keyEquivalent: &*NSString::from_str(""), - ]; - Id::from_raw(raw).expect("NSMenuItem init returned nil") - } + fn new_menu_item( + title: &str, + action: Option, + mtm: MainThreadMarker, + ) -> Retained { + let item = NSMenuItem::new(mtm); + item.setTitle(&NSString::from_str(title)); + unsafe { item.setAction(action) }; + item.setKeyEquivalent(&NSString::from_str("")); + item } /// `RecordingStarted` / `RecordingStopped`. @@ -991,27 +1253,24 @@ mod mac { } cx.global_mut::().recording = recording; let tray = cx.global::(); - unsafe { - let button: *mut AnyObject = msg_send![&*tray.status_item, button]; - if recording { - let _: () = - msg_send![&*tray.status_item, setMenu: std::ptr::null_mut::()]; - if !button.is_null() { - let _: () = msg_send![button, setTarget: &*tray.target]; - let _: () = msg_send![button, setAction: Some(sel!(capTrayStop:))]; - } - set_button_image(tray, STOP_ICON); - } else { - if !button.is_null() { - let _: () = msg_send![button, setAction: None::]; - let _: () = msg_send![button, setTarget: std::ptr::null_mut::()]; - } - if let Some(menu) = &tray.menu { - let _: () = msg_send![&*tray.status_item, setMenu: &**menu]; - } - let icon = mode_icon(tray.mode); - set_button_image(tray, icon); + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let button = tray.status_item.button(mtm); + if recording { + tray.status_item.setMenu(None); + if let Some(button) = button { + unsafe { button.setTarget(Some(&*tray.target)) }; + unsafe { button.setAction(Some(sel!(capTrayStop:))) }; + } + set_button_image(tray, STOP_ICON); + } else { + if let Some(button) = button { + unsafe { button.setAction(None) }; + unsafe { button.setTarget(None) }; } + tray.status_item.setMenu(tray.menu.as_deref()); + set_button_image(tray, mode_icon(tray.mode)); } tracing::info!(recording, "tray recording state"); } @@ -1318,6 +1577,8 @@ mod tests { .iter() .map(|entry| match entry { Entry::Separator => "-".to_string(), + Entry::ModePicker { mode } => mode_name(*mode).to_string(), + Entry::SectionHeader { title } => title.clone(), Entry::Item { title, .. } => title.clone(), Entry::Submenu { title, .. } => title.clone(), }) @@ -1336,28 +1597,37 @@ mod tests { #[test] fn menu_matches_build_tray_menu() { let menu = build_menu(Mode::Studio, &[], "0.1.0"); - assert_eq!( - titles(&menu), - vec![ - "Open Main Window", - "Record Display", - "Record Window", - "Record Area", - "Take a Screenshot", - "Import Media...", - "-", - "Select Mode", - "Previous", - "-", - "View all recordings", - "View all screenshots", - "Settings", - "-", - "Upload Logs", - "Cap v0.1.0", - "Quit Cap", - ] - ); + // macOS renders the group as a section header naming the current + // mode; the `mode_submenu` other platforms get keeps its group label. + let mode_label = if cfg!(target_os = "macos") { + "Studio" + } else { + "Recording Mode" + }; + let mut expected = vec!["Open Main Window", "-", mode_label, "-"]; + expected.extend([ + "Record Display", + "Record Window", + "Record Area", + "Take a Screenshot", + "-", + "Previous", + "View all recordings", + "View all screenshots", + "-", + "Import Media…", + "Settings…", + "Upload Logs", + "-", + ]); + // The version row is a section header below Quit on macOS and a + // disabled row above it everywhere else. + if cfg!(target_os = "macos") { + expected.extend(["Quit Cap", "Cap v0.1.0"]); + } else { + expected.extend(["Cap v0.1.0", "Quit Cap"]); + } + assert_eq!(titles(&menu), expected); } /// Screenshot mode relabels the three target rows and drops "Take a @@ -1370,11 +1640,10 @@ mod tests { &titles[..6], &[ "Open Main Window", + "-", + "-", "Screenshot Display", "Screenshot Window", - "Screenshot Area", - "Import Media...", - "-", ] ); assert!(!titles.iter().any(|title| title == "Take a Screenshot")); @@ -1397,7 +1666,7 @@ mod tests { ), ] { let Entry::Submenu { items, .. } = mode_submenu(mode) else { - panic!("Select Mode is a submenu"); + panic!("the mode picker is a submenu"); }; assert_eq!(titles(&items), expected.to_vec()); } @@ -1409,6 +1678,7 @@ mod tests { title, enabled, items, + .. } = previous_submenu(&[]) else { panic!("Previous is a submenu"); @@ -1466,6 +1736,13 @@ mod tests { _ => None, }) .collect(); - assert_eq!(disabled, vec!["Upload Logs", "Cap v0.1.0"]); + assert_eq!( + disabled, + if cfg!(target_os = "macos") { + vec!["Upload Logs"] + } else { + vec!["Upload Logs", "Cap v0.1.0"] + } + ); } } diff --git a/apps/desktop-gpui/src/tray/mac_mode_picker.rs b/apps/desktop-gpui/src/tray/mac_mode_picker.rs new file mode 100644 index 00000000000..1817571570f --- /dev/null +++ b/apps/desktop-gpui/src/tray/mac_mode_picker.rs @@ -0,0 +1,450 @@ +//! The mode picker row: a title and subtitle on the left, a capsule of pills on the right. +//! +//! Layers (`CALayer`) draw the capsule and pills because only they have continuous +//! corners; icons are image views above them, and `drawRect:` draws just the text. +//! Nothing in here touches gpui: a pick only posts a tag through [`post`], the same +//! seam every other tray action uses. + +use std::cell::{Cell, RefCell}; + +use block2::RcBlock; +use objc2::rc::Retained; +use objc2::runtime::{AnyClass, AnyObject, Bool, Sel}; +use objc2::{AnyThread, DefinedClass, MainThreadOnly, define_class, msg_send, sel}; +use objc2_app_kit::{ + NSAppearance, NSColor, NSEvent, NSEventType, NSFont, NSFontAttributeName, NSFontManager, + NSFontTraitMask, NSFontWeightMedium, NSFontWeightSemibold, NSForegroundColorAttributeName, + NSImageScaling, NSImageSymbolConfiguration, NSImageView, NSMenu, NSMenuItem, NSStringDrawing, + NSTrackingArea, NSTrackingAreaOptions, NSView, NSAppearanceCustomization, +}; +use objc2_foundation::{MainThreadMarker, NSDictionary, NSPoint, NSRect, NSSize, NSString}; +use objc2_quartz_core::{CALayer, CATransaction, kCACornerCurveContinuous}; + +use super::mac::{post, symbol_image}; +use super::{TrayItem, mode_name}; +use crate::main_window::Mode; + +const PICKER_MODES: [(Mode, TrayItem, &str); 3] = [ + (Mode::Instant, TrayItem::ModeInstant, "bolt.fill"), + (Mode::Studio, TrayItem::ModeStudio, "film"), + (Mode::Screenshot, TrayItem::ModeScreenshot, "photo"), +]; + +const PICKER_TITLE: &str = "Recording Mode"; +const PICKER_W: f64 = 300.0; +const PICKER_H: f64 = 41.0; +const PILL_W: f64 = 38.0; +const PILL_H: f64 = 29.0; +const PILL_GAP: f64 = 4.0; +const CAPSULE_PAD: f64 = 3.0; +const RIGHT_INSET: f64 = 10.0; +const TITLE_INSET: f64 = 17.0; +const LINE_GAP: f64 = 2.0; +const SYMBOL_SIZE: f64 = 17.0; +const PILL_RADIUS: f64 = 14.0; +const CAPSULE_RADIUS: f64 = 17.0; + +thread_local! { + /// The live picker and its menu item, read by the key hook. Set on every rebuild. + static PICKER: RefCell, Retained)>> = + const { RefCell::new(None) }; +} + +struct PickerIvars { + selected: Cell, + focused: Cell>, + /// `actions` index of the first pill; pill `i` posts `tag_base + i`. + tag_base: isize, + capsule: RefCell>>, + pills: RefCell>>, + icons: RefCell>>, +} + +define_class!( + // SAFETY: NSView has no extra subclassing requirements; ivars have no Drop side effects. + #[unsafe(super(NSView))] + #[name = "CapModePickerView"] + #[thread_kind = MainThreadOnly] + #[ivars = PickerIvars] + struct ModePickerView; + + impl ModePickerView { + #[unsafe(method(acceptsFirstMouse:))] + fn accepts_first_mouse(&self, _event: Option<&NSEvent>) -> bool { + true + } + + #[unsafe(method(viewDidMoveToWindow))] + fn view_did_move_to_window(&self) { + unsafe { + let _: () = msg_send![super(self), viewDidMoveToWindow]; + } + if let Some(window) = self.window() { + window.setAcceptsMouseMovedEvents(true); + } + self.sync(); + } + + #[unsafe(method(viewDidChangeEffectiveAppearance))] + fn view_did_change_effective_appearance(&self) { + unsafe { + let _: () = msg_send![super(self), viewDidChangeEffectiveAppearance]; + } + self.sync(); + } + + #[unsafe(method(mouseMoved:))] + fn mouse_moved(&self, event: &NSEvent) { + let hit = self.pill_at(event); + if self.ivars().focused.get() != hit { + self.ivars().focused.set(hit); + self.refresh(); + } + } + + #[unsafe(method(mouseExited:))] + fn mouse_exited(&self, _event: &NSEvent) { + self.ivars().focused.set(None); + self.refresh(); + } + + #[unsafe(method(mouseDown:))] + fn mouse_down(&self, event: &NSEvent) { + if let Some(i) = self.pill_at(event) { + // Update the subtitle immediately so the old name never flashes. + self.ivars().focused.set(Some(i)); + self.ivars().selected.set(i); + self.refresh(); + } + } + + #[unsafe(method(mouseUp:))] + fn mouse_up(&self, event: &NSEvent) { + if let Some(i) = self.pill_at(event) { + self.choose(i); + } + } + + #[unsafe(method(drawRect:))] + fn draw_rect(&self, _dirty: NSRect) { + self.draw_text(); + } + } +); + +impl ModePickerView { + fn new(mtm: MainThreadMarker, selected: usize, tag_base: isize) -> Retained { + let this = mtm.alloc::().set_ivars(PickerIvars { + selected: Cell::new(selected), + focused: Cell::new(None), + tag_base, + capsule: RefCell::new(None), + pills: RefCell::new(Vec::new()), + icons: RefCell::new(Vec::new()), + }); + let frame = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(PICKER_W, PICKER_H)); + // Initialise first; everything below touches the live view. + let this: Retained = unsafe { msg_send![super(this), initWithFrame: frame] }; + + this.setWantsLayer(true); + let root = this.layer().expect("layer-backed view"); + let make_layer = |radius: f64| -> Retained { + let layer = CALayer::layer(); + layer.setCornerCurve(unsafe { kCACornerCurveContinuous }); + layer.setCornerRadius(radius); + root.addSublayer(&layer); + layer + }; + *this.ivars().capsule.borrow_mut() = Some(make_layer(CAPSULE_RADIUS)); + for _ in PICKER_MODES.iter() { + this.ivars().pills.borrow_mut().push(make_layer(PILL_RADIUS)); + } + + for (_, _, symbol) in PICKER_MODES.iter() { + let view = NSImageView::new(mtm); + if let Some(base) = symbol_image(symbol) { + let cfg = NSImageSymbolConfiguration::configurationWithPointSize_weight( + SYMBOL_SIZE, + unsafe { NSFontWeightMedium }, + ); + view.setImage(base.imageWithSymbolConfiguration(&cfg).as_deref()); + } + view.setImageScaling(NSImageScaling::ScaleNone); + this.addSubview(&view); + this.ivars().icons.borrow_mut().push(view); + } + + let options = NSTrackingAreaOptions::MouseMoved + | NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::ActiveAlways + | NSTrackingAreaOptions::InVisibleRect; + let area = unsafe { + NSTrackingArea::initWithRect_options_owner_userInfo( + NSTrackingArea::alloc(), + NSRect::ZERO, + options, + Some(&this), + None, + ) + }; + this.addTrackingArea(&area); + + this.sync(); + this + } + + // -- geometry ---------------------------------------------------------- + + fn capsule_rect(&self) -> NSRect { + let n = PICKER_MODES.len() as f64; + let w = n * PILL_W + (n - 1.0) * PILL_GAP + 2.0 * CAPSULE_PAD; + let h = PILL_H + 2.0 * CAPSULE_PAD; + let bounds = self.bounds(); + NSRect::new( + NSPoint::new(bounds.size.width - RIGHT_INSET - w, (bounds.size.height - h) / 2.0), + NSSize::new(w, h), + ) + } + + fn pill_rect(&self, i: usize) -> NSRect { + let c = self.capsule_rect(); + NSRect::new( + NSPoint::new( + c.origin.x + CAPSULE_PAD + i as f64 * (PILL_W + PILL_GAP), + c.origin.y + CAPSULE_PAD, + ), + NSSize::new(PILL_W, PILL_H), + ) + } + + fn pill_at(&self, event: &NSEvent) -> Option { + let p = self.convertPoint_fromView(event.locationInWindow(), None); + (0..PICKER_MODES.len()).find(|&i| { + let r = self.pill_rect(i); + p.x >= r.origin.x - PILL_GAP / 2.0 + && p.x <= r.origin.x + r.size.width + PILL_GAP / 2.0 + && p.y >= r.origin.y + && p.y <= r.origin.y + r.size.height + }) + } + + // -- state -> visuals -------------------------------------------------- + + /// Layers and icons for the current state, then the text. + fn refresh(&self) { + self.sync(); + self.setNeedsDisplay(true); + } + + /// The only place state becomes layer properties. + fn sync(&self) { + let selected = self.ivars().selected.get(); + let focused = self.ivars().focused.get(); + + // CGColors don't follow appearance, so resolve the dynamic colours under this + // view's appearance. (Deprecated, but it has no block-free replacement.) + #[allow(deprecated)] + unsafe { NSAppearance::setCurrentAppearance(Some(&self.effectiveAppearance())) }; + + let label = NSColor::labelColor(); + CATransaction::begin(); + CATransaction::setDisableActions(true); + + if let Some(cap) = self.ivars().capsule.borrow().as_ref() { + cap.setFrame(self.capsule_rect()); + let color = label.colorWithAlphaComponent(0.08).CGColor(); + cap.setBackgroundColor(Some(&color)); + } + + let icons = self.ivars().icons.borrow(); + for (i, pill) in self.ivars().pills.borrow().iter().enumerate() { + let is_sel = i == selected; + let is_hover = focused == Some(i) && !is_sel; + let fill = if is_sel { + NSColor::whiteColor() + } else if is_hover { + label.colorWithAlphaComponent(0.14) + } else { + NSColor::clearColor() + }; + let r = self.pill_rect(i); + pill.setFrame(r); + let color = fill.CGColor(); + pill.setBackgroundColor(Some(&color)); + + if let Some(icon) = icons.get(i) { + icon.setFrame(r); + let tint = if is_sel { + NSColor::selectedContentBackgroundColor() + } else { + label.clone() + }; + icon.setContentTintColor(Some(&tint)); + } + } + CATransaction::commit(); + } + + // -- drawing: text only ------------------------------------------------ + + fn draw_text(&self) { + let selected = self.ivars().selected.get(); + let focused = self.ivars().focused.get(); + let shown = focused.unwrap_or(selected); + + let menu_size = NSFont::menuFontOfSize(0.0).pointSize(); + let title_font = NSFont::systemFontOfSize_weight(menu_size, unsafe { NSFontWeightSemibold }); + let mut sub_font = NSFont::systemFontOfSize(menu_size - 2.0); + if shown != selected { + sub_font = NSFontManager::sharedFontManager( + MainThreadMarker::new().expect("draw runs on the main thread"), + ) + .convertFont_toHaveTrait(&sub_font, NSFontTraitMask::ItalicFontMask); + } + + let label = NSColor::labelColor(); + let secondary = NSColor::secondaryLabelColor(); + let title = NSString::from_str(PICKER_TITLE); + let subtitle = NSString::from_str(mode_name(PICKER_MODES[shown].0)); + let title_attrs = NSDictionary::from_slices( + &[unsafe { NSFontAttributeName }, unsafe { NSForegroundColorAttributeName }], + &[&*title_font as &AnyObject, &*label as &AnyObject], + ); + let sub_attrs = NSDictionary::from_slices( + &[unsafe { NSFontAttributeName }, unsafe { NSForegroundColorAttributeName }], + &[&*sub_font as &AnyObject, &*secondary as &AnyObject], + ); + + let ts = unsafe { title.sizeWithAttributes(Some(&title_attrs)) }; + let ss = unsafe { subtitle.sizeWithAttributes(Some(&sub_attrs)) }; + let base_y = (self.bounds().size.height - (ts.height + ss.height + LINE_GAP)) / 2.0; + unsafe { + subtitle + .drawAtPoint_withAttributes(NSPoint::new(TITLE_INSET, base_y), Some(&sub_attrs)); + title.drawAtPoint_withAttributes( + NSPoint::new(TITLE_INSET, base_y + ss.height + LINE_GAP), + Some(&title_attrs), + ); + } + } + + // -- actions ----------------------------------------------------------- + + /// Selects pill `i` and posts its tag. Deliberately does not close the menu. + fn choose(&self, i: usize) { + self.ivars().selected.set(i); + self.ivars().focused.set(Some(i)); + self.refresh(); + post(self.ivars().tag_base + i as isize); + } + + /// Moves keyboard focus one pill. Returns false at either end, so the key falls + /// through to the menu and moves to the neighbouring tray menu. + fn move_focus(&self, delta: isize) -> bool { + let cur = self.ivars().focused.get().unwrap_or(self.ivars().selected.get()) as isize; + let next = cur + delta; + if next < 0 || next >= PICKER_MODES.len() as isize { + return false; + } + self.ivars().focused.set(Some(next as usize)); + self.refresh(); + true + } + + fn activate_focused(&self) { + let i = self.ivars().focused.get().unwrap_or(self.ivars().selected.get()); + self.choose(i); + } +} + +/// Appends the mode picker row to `menu` and registers one action per pill, so a click +/// arrives through the usual tag channel as `ModeInstant` / `ModeStudio` / `ModeScreenshot`. +pub(super) fn append_mode_picker( + menu: &NSMenu, + selected: Mode, + actions: &mut Vec, + mtm: MainThreadMarker, +) -> Retained { + let tag_base = actions.len() as isize; + actions.extend(PICKER_MODES.iter().map(|(_, item, _)| item.clone())); + + let index = PICKER_MODES.iter().position(|(m, _, _)| *m == selected).unwrap_or(0); + let view = ModePickerView::new(mtm, index, tag_base); + + let item = NSMenuItem::new(mtm); + item.setTitle(&NSString::from_str(PICKER_TITLE)); // also its accessibility title + item.setEnabled(true); + item.setView(Some(&view)); + menu.addItem(&item); + + PICKER.with(|p| *p.borrow_mut() = Some((item.clone(), view))); + item +} + +// --------------------------------------------------------------------------- +// Private keyboard hook +// --------------------------------------------------------------------------- + +/// ←/→/Return/Space while the picker row is highlighted. True means consumed. +fn handle_key(event: &NSEvent) -> bool { + PICKER.with(|p| { + let p = p.borrow(); + let Some((item, view)) = p.as_ref() else { + return false; + }; + if !item.isHighlighted() { + return false; + } + match event.keyCode() { + 123 => view.move_focus(-1), + 124 => view.move_focus(1), + 36 | 76 | 49 => { + view.activate_focused(); + true + } + _ => false, + } + }) +} + +/// Wraps `NSPopupMenuWindow -_handleKeyEvent:`. Private API: it does nothing if the +/// method is missing or has another shape, and it may break on any macOS update. +pub(super) fn install_key_hook() { + let sel = sel!(_handleKeyEvent:); + let Some(cls) = AnyClass::get(c"NSPopupMenuWindow") else { + return; + }; + let Some(method) = cls.instance_method(sel) else { + return; + }; + let enc = unsafe { + let p = objc2::ffi::method_getTypeEncoding( + method as *const _ as *const objc2::ffi::objc_method, + ); + if p.is_null() { return; } + std::ffi::CStr::from_ptr(p) + }; + if enc.to_bytes() != b"B24@0:8@16" { + tracing::warn!(encoding = ?enc, "key hook: unexpected encoding, not installing"); + return; + } + let orig = method.implementation(); + + let block = RcBlock::new(move |this: *mut AnyObject, event: *mut NSEvent| -> Bool { + let ev = unsafe { &*event }; + if ev.r#type() == NSEventType::KeyDown && handle_key(ev) { + return Bool::YES; + } + let orig: unsafe extern "C-unwind" fn(*mut AnyObject, Sel, *mut NSEvent) -> Bool = + unsafe { std::mem::transmute(orig) }; + unsafe { orig(this, sel, event) } + }); + unsafe { + let imp = objc2::ffi::imp_implementationWithBlock( + &*block as *const _ as *mut AnyObject, + ); + method.set_implementation(std::mem::transmute(imp)); + } + std::mem::forget(block); // the replacement must outlive the process + tracing::info!("key hook installed"); +} diff --git a/apps/desktop-gpui/src/tray/windows.rs b/apps/desktop-gpui/src/tray/windows.rs index 04de1870557..a3d352f77ea 100644 --- a/apps/desktop-gpui/src/tray/windows.rs +++ b/apps/desktop-gpui/src/tray/windows.rs @@ -155,6 +155,7 @@ fn build_native_item( ) -> anyhow::Result> { match entry { Entry::Separator => Ok(Box::new(PredefinedMenuItem::separator())), + Entry::ModePicker { mode } => build_native_item(&super::mode_submenu(*mode), actions), Entry::Item { title, item, @@ -185,10 +186,14 @@ fn build_native_item( } Ok(native_item) } + Entry::SectionHeader { title } => { + Ok(Box::new(MenuItem::new(menu_text(title), false, None))) + } Entry::Submenu { title, enabled, items, + .. } => { let submenu = Submenu::new(menu_text(title), *enabled); for entry in items { diff --git a/apps/desktop-gpui/src/updates.rs b/apps/desktop-gpui/src/updates.rs index ae48b344e1f..c866bae6e21 100644 --- a/apps/desktop-gpui/src/updates.rs +++ b/apps/desktop-gpui/src/updates.rs @@ -21,6 +21,7 @@ const BUSY_MESSAGE: &str = pub(crate) enum UpdateStatus { #[default] Idle, + Checking, Downloading { version: Version, fraction: Option, @@ -173,6 +174,7 @@ fn set_status(status: UpdateStatus, cx: &mut App) { } scheduler.status = status; crate::app_windows::refresh_settings(cx); + crate::menus::refresh_update_menu(cx); } pub(crate) fn check_manually(cx: &mut App) { @@ -196,7 +198,7 @@ pub(crate) fn check_manually(cx: &mut App) { .detach(); return; } - UpdateStatus::Idle => {} + UpdateStatus::Idle | UpdateStatus::Checking => {} } let scheduler = cx.global_mut::(); @@ -221,7 +223,7 @@ pub(crate) fn update_channel_changed(channel: UpdateChannel, cx: &mut App) { fn channel_switch_drops_pending_package(status: &UpdateStatus) -> bool { matches!( status, - UpdateStatus::Downloading { .. } | UpdateStatus::Ready { .. } + UpdateStatus::Checking | UpdateStatus::Downloading { .. } | UpdateStatus::Ready { .. } ) } @@ -313,7 +315,7 @@ pub(crate) fn schedule_startup_check(cx: &mut App) { if let Some(next_channel) = request.channel { channel = next_channel; ignored_version = None; - let _ = cx.update(drop_pending_update_from_previous_channel); + cx.update(drop_pending_update_from_previous_channel); } let manual = request.manual; @@ -333,6 +335,8 @@ pub(crate) fn schedule_startup_check(cx: &mut App) { continue; } + cx.update(|cx| set_status(UpdateStatus::Checking, cx)); + let result = match cx .update(|cx| gpui_tokio::Tokio::spawn(cx, available_release(channel))) .await @@ -351,12 +355,14 @@ pub(crate) fn schedule_startup_check(cx: &mut App) { channel_changed || (!manual && scheduler.pending.manual) }); if superseded { + cx.update(|cx| set_status(UpdateStatus::Idle, cx)); continue; } let release = match result { Ok(Some(release)) => release, Ok(None) => { + cx.update(|cx| set_status(UpdateStatus::Idle, cx)); if manual { crate::platform::activate_app(); crate::platform::alert_dialog( @@ -368,6 +374,7 @@ pub(crate) fn schedule_startup_check(cx: &mut App) { continue; } Err(error) => { + cx.update(|cx| set_status(UpdateStatus::Idle, cx)); tracing::warn!("update check failed: {error}"); if manual { crate::platform::activate_app(); @@ -382,6 +389,7 @@ pub(crate) fn schedule_startup_check(cx: &mut App) { }; if !manual && ignored_version.as_ref() == Some(&release.version) { + cx.update(|cx| set_status(UpdateStatus::Idle, cx)); continue; } @@ -409,6 +417,8 @@ pub(crate) fn schedule_startup_check(cx: &mut App) { continue; } + cx.update(|cx| set_status(UpdateStatus::Idle, cx)); + crate::platform::activate_app(); let accepted = crate::platform::confirm_dialog( "Update Cap", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1b7ebd5e966..fca2b4f5189 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -45,7 +45,7 @@ "@solidjs/router": "^0.14.2", "@solidjs/start": "^1.1.3", "@tanstack/solid-query": "^5.51.21", - "@tauri-apps/api": "2.8.0", + "@tauri-apps/api": "2.11.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-deep-link": "^2.4.1", "@tauri-apps/plugin-dialog": "2.4.2", @@ -82,7 +82,7 @@ "@solid-devtools/overlay": "^0.33.5", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.9", - "@tauri-apps/cli": "2.8.4", + "@tauri-apps/cli": "2.11.0", "@total-typescript/ts-reset": "^0.6.1", "@types/dom-webcodecs": "^0.1.11", "@types/uuid": "^9.0.8", diff --git a/bun.lock b/bun.lock index d8e8c7cddb2..c16651409a3 100644 --- a/bun.lock +++ b/bun.lock @@ -82,7 +82,7 @@ "@solidjs/router": "^0.14.2", "@solidjs/start": "^1.1.3", "@tanstack/solid-query": "^5.51.21", - "@tauri-apps/api": "2.8.0", + "@tauri-apps/api": "2.12.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-deep-link": "^2.4.1", "@tauri-apps/plugin-dialog": "2.4.2", @@ -119,7 +119,7 @@ "@solid-devtools/overlay": "^0.33.5", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.9", - "@tauri-apps/cli": "2.8.4", + "@tauri-apps/cli": "2.12.0", "@total-typescript/ts-reset": "^0.6.1", "@types/dom-webcodecs": "^0.1.11", "@types/uuid": "^9.0.8", @@ -2399,31 +2399,31 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], - "@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + "@tauri-apps/api": ["@tauri-apps/api@2.12.0", "", {}, "sha512-fUSxFX8VABYe9W9K5ROdEaKVkNUFRcMSRofjruvaxztXA+I0XnQHcYHF8m/a6iExRUxsUf0HsFWhPBuH9IgzJw=="], - "@tauri-apps/cli": ["@tauri-apps/cli@2.8.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.8.4", "@tauri-apps/cli-darwin-x64": "2.8.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.8.4", "@tauri-apps/cli-linux-arm64-gnu": "2.8.4", "@tauri-apps/cli-linux-arm64-musl": "2.8.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-musl": "2.8.4", "@tauri-apps/cli-win32-arm64-msvc": "2.8.4", "@tauri-apps/cli-win32-ia32-msvc": "2.8.4", "@tauri-apps/cli-win32-x64-msvc": "2.8.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g=="], + "@tauri-apps/cli": ["@tauri-apps/cli@2.12.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.12.0", "@tauri-apps/cli-darwin-x64": "2.12.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.0", "@tauri-apps/cli-linux-arm64-gnu": "2.12.0", "@tauri-apps/cli-linux-arm64-musl": "2.12.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.12.0", "@tauri-apps/cli-linux-x64-gnu": "2.12.0", "@tauri-apps/cli-linux-x64-musl": "2.12.0", "@tauri-apps/cli-win32-arm64-msvc": "2.12.0", "@tauri-apps/cli-win32-ia32-msvc": "2.12.0", "@tauri-apps/cli-win32-x64-msvc": "2.12.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-GIShnAR4C16kSTA8NO02fo8fOw/I5z5sAUEB5S2HlPHFnBkbihL4c9a/MpkxOm1+q5S+tFd3X4QUjHWxfyGzpQ=="], - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.8.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA=="], + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.12.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v5J+jISihLGtih1+3yWagbsL+5dbLZORu4vaSI0vhY0Heb+MAW+T/1s1RyKxrQrwEPGBG5kqwfDDTnSbmVkuqA=="], - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.8.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g=="], + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.12.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-g27I07pfW/dMS8vcNqB9lamgKF0TqLlcy8e7t9qDGsB1iaeyjJWb2TgBF4tM9ZwLqPAZUXbsz28oXljO12WC4g=="], - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.8.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw=="], + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.12.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gXyQ710fNc99YuxFxNyeX9PLSrIpbtgBBj7GQo9tSeRT6fIK40q3/gm7X5jqZo7mWLR2m0R/2ALd3vAWo0Z32Q=="], - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw=="], + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-H9cyXuq9WgxoHh2NZ+R+oN+W78x5UwaBDEp0/fmqUUkaBoioCuOZErM9xKiIV6bpNAhmw1RoWZ49u37q/vaiAg=="], - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw=="], + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5tFuk3JKMBa1M/iN4gN32OYlSfhY3aqb5PO3zCugxaT+sfecyMKpc3EM9VDbhmwWBE9QD4Tfj7wGhlqx2yy2pQ=="], - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.8.4", "", { "os": "linux", "cpu": "none" }, "sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ=="], + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.12.0", "", { "os": "linux", "cpu": "none" }, "sha512-LOrMyV+nb7pKq8P0BZtTBQuyzm1ORjn16R9MlupmxuT5LqxtuYJlw3VMwbaeXCToJ8jbP5u9JnkfmbqDlvETXw=="], - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw=="], + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-aMIkI+X9qvD2+k80QZB3tuOYT41/cEheikC510CwAIZ31KLXORsRASYYzM4mi1/58ICmtzSLFhCC08keir7COw=="], - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA=="], + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9gl3NhUbfuGhSKjeGCypKSdtoezOQRcDUrkhTWtLQEjnajets6peO6NadgI2DF0OzbiiFOzrnnIGMwBS+Ap6Iw=="], - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.8.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w=="], + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.12.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1W+v86wsYLuyBDN0iDessD4CD9o+yNyO0m8Dp3+/vfjiZrJ/v7R7/aTRBeikaq2tLGgxeag0gxnFggBNBGw91A=="], - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.8.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA=="], + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.12.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SsBjvq4f7YaBGqcWoE3cycoItPf0SrQeqFqWWgfxoeZWAIz01E31LDSeZ2p/etJiIY0R9oi045chkFfb0O7WGw=="], - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.8.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA=="], + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-9zzDKYUH4TKKBgRdDdVQX8fiRO5gJ8hD1nGsHU9DOHG5Jpm3arjbu9Xg1frR20EusG3Sl8X0dppoO/4EoUXUGg=="], "@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="], @@ -6381,14 +6381,30 @@ "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "7.29.7", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + "@tauri-apps/plugin-clipboard-manager/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + "@tauri-apps/plugin-deep-link/@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], + "@tauri-apps/plugin-dialog/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + + "@tauri-apps/plugin-fs/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + + "@tauri-apps/plugin-http/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + + "@tauri-apps/plugin-notification/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + "@tauri-apps/plugin-opener/@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], + "@tauri-apps/plugin-os/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + + "@tauri-apps/plugin-process/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + "@tauri-apps/plugin-shell/@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], "@tauri-apps/plugin-store/@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], + "@tauri-apps/plugin-updater/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + "@tufjs/models/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.7" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "@types/dom-mediacapture-transform/@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],