diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 000000000..6bfd362b3 --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,95 @@ +# Hack desktop app (HACK-2214): unsupported preview. +# Separate from the default gates and the release workflow, and not a +# required check. Linux and macOS builds of the app plus the `core` tests. +# The toolchain comes from apps/desktop/rust-toolchain.toml (1.98.1; macOS +# needs it), not from the repo root pin. +name: Desktop app + +on: + pull_request: + paths: + - "apps/desktop/**" + - ".github/workflows/desktop.yml" + push: + branches: [next] + paths: + - "apps/desktop/**" + - ".github/workflows/desktop.yml" + +permissions: + contents: read + +concurrency: + group: desktop-${{ github.ref }} + cancel-in-progress: true + +jobs: + linux: + name: Desktop app (Linux build, core tests) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + defaults: + run: + working-directory: apps/desktop + steps: + - uses: actions/checkout@v4 + - name: Install GPUI build dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + clang libfontconfig-dev libwayland-dev libxkbcommon-x11-dev \ + libx11-xcb-dev libssl-dev libzstd-dev libvulkan-dev pkg-config + - uses: dtolnay/rust-toolchain@1.98.1 + with: + components: rustfmt, clippy + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + apps/desktop/target + key: desktop-${{ runner.os }}-${{ hashFiles('apps/desktop/Cargo.lock') }} + restore-keys: desktop-${{ runner.os }}- + - name: Format + run: cargo fmt --all --check + - name: Core tests (no daemon, no Docker) + env: + HACK_DAEMON_AUTOSTART_DISABLE: "1" + run: cargo test --locked -p hack-desktop-core + # Both: the screenshots feature turns on gpui test-support, which can + # hide test-only APIs from a build that only uses that feature. + - name: Clippy (default features) + run: cargo clippy --locked --all-targets -- -D warnings + - name: Clippy (screenshots feature) + run: cargo clippy --locked --all-targets --features hack-desktop/screenshots -- -D warnings + - name: Build app + run: cargo build --locked -p hack-desktop + + macos: + name: Desktop app (macOS build, core tests) + runs-on: macos-15 + timeout-minutes: 40 + defaults: + run: + working-directory: apps/desktop + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.98.1 + with: + components: clippy + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + apps/desktop/target + key: desktop-${{ runner.os }}-${{ hashFiles('apps/desktop/Cargo.lock') }} + restore-keys: desktop-${{ runner.os }}- + - name: Core tests (no daemon, no Docker) + env: + HACK_DAEMON_AUTOSTART_DISABLE: "1" + run: cargo test --locked -p hack-desktop-core + - name: Clippy (default features) + run: cargo clippy --locked --all-targets -- -D warnings + - name: Build app + run: cargo build --locked -p hack-desktop diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore new file mode 100644 index 000000000..b5cf404cb --- /dev/null +++ b/apps/desktop/.gitignore @@ -0,0 +1,2 @@ +/target/ +/screenshots/out/ diff --git a/apps/desktop/Cargo.lock b/apps/desktop/Cargo.lock new file mode 100644 index 000000000..7074abb5e --- /dev/null +++ b/apps/desktop/Cargo.lock @@ -0,0 +1,9222 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf 0.13.1", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus 5.19.0", +] + +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "annotate-snippets" +version = "0.12.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0258346f92c78dbc5b336b9a5b27c64447aa10643bc64e651db2edb145361564" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "arc-swap" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.19.0", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f426a31126c735126297bc5ea7a521263c9faff4f63ca415267ee625651c4f37" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0876700b0d007ad25d5508865a4259c3af9af6d296bf663a0d586d0bc3369f6f" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus 5.19.0", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus 5.19.0", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.21", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f4220217f3288537b83684130f42fad43320b1c3b805f1910d683d52ded24f" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.9.1", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base62" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323" + +[[package]] +name = "base64" +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 = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec 0.9.1", +] + +[[package]] +name = "bit-set" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec 0.10.2", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit-vec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72503af1e72be411d74ca45cd6b309878fcb35354e252fc2a3a8bda3a97082a0" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "bit_field" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb2907cc03d08cdb1c6724ad0670ce67c4a71e1c45a6d7635224011714cfc4b1" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.5", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a0cf66fd1eb2fb7b822cb78370d82303750809bc6ba855f3f58e517d3238db" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "c-enum" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8" + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ef9e3e8154a45c7a95cff4c1299702106ad94062d7bb85fdc5d3a5afd91d45" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be95d4a884358a21a199689323c29e9256b4a1b315e22f183d155422041d3a9a" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.2", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b929dd2a135771f40afabd1073f85eed59c800b7c61901e5c254270497dad64" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.2.1+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35d6e78f4cebb90e51d543db8fe41528851d6deeef6ef21ebd4bfdc1e784776" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb29d0be702adf92c645d9b2f93472906ba83d400e59f98adb3770ce5c18c2f6" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "multiversion", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fixedbitset" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5905ec755cc8386d7928353e4d356d2be3e89e81e5ca01fe630a1ca4d08dc175" + +[[package]] +name = "flate2" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626d4928326b646e70f65ed36fc7cf9f206bdb68a0019ee7c6b4709da4e084c4" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98b0d85522dc0a824cfebbc9594ff4a9b289c568bea0370319dfbdf20ccdd2d" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows 0.62.2", +] + +[[package]] +name = "gpui-base" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e9ae5237737cfd110d0ff513fcd9c20f97d98ebabd0a73164068177f0aca4f" +dependencies = [ + "aho-corasick", + "anyhow", + "ashpd", + "async-channel", + "chrono", + "data-url", + "futures", + "gpui-fast", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "html5ever", + "instant", + "itertools 0.13.0", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "num-traits", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "raw-window-handle", + "regex", + "ropey", + "schemars", + "serde", + "serde_json", + "smallvec", + "smol", + "tracing", + "unicode-linebreak", + "unicode-segmentation", + "web-time", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e155c4914bd809d966660610d596f7b57c5cd38979ca2c132215057a91f23f03" +dependencies = [ + "anyhow", + "chrono", + "core-text", + "enum-iterator", + "gpui-base", + "gpui-component-macros", + "gpui-fast", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "instant", + "itertools 0.13.0", + "log", + "lsp-types", + "markdown", + "notify", + "num-traits", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "paste", + "raw-window-handle", + "regex", + "resvg 0.45.1", + "ropey", + "rust-i18n", + "schemars", + "serde", + "serde_json", + "serde_repr", + "similar", + "smallvec", + "smol", + "tracing", + "unicode-segmentation", + "unicode-width", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component-macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676a5d12c59ce295b5c82cc18365ad8b96cb683682e2e74668a56fb92544300" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-fast" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a069272b90d908d15152711b38153edabcf67a5dcc490123197af7fd9ad78d9d" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bitflags 2.13.2", + "chrono", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-fast-macros", + "gpui-pre-bench-metrics", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "heapless", + "icu_segmenter", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "taffy", + "thiserror 2.0.21", + "ttf-parser", + "unicode-segmentation", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-scap", +] + +[[package]] +name = "gpui-fast-apple" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94395e1c8cc1e0795d3bac933d8701582eb3e6b274db09919dca24eab379d801" +dependencies = [ + "anyhow", + "async-task", + "block2 0.6.2", + "cbindgen", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "dispatch2", + "etagere", + "futures", + "gpui-fast", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "libc", + "log", + "mach2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-security", + "parking_lot", + "pathfinder_geometry", + "smallvec", + "thiserror 2.0.21", + "zed-font-kit", +] + +[[package]] +name = "gpui-fast-linux" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92c93a4828b4d54f47bad288950da47f2fbaa51bd8b845ac5ef14d86c5f7c8e" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui-fast", + "gpui-fast-wgpu", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-fast-macos" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33d8962910c761956b806a318f8ae4d2b582066cc440a6be3502ddaaa1272cb" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui-fast", + "gpui-fast-apple", + "gpui-pre-collections", + "gpui-pre-scheduler", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", + "parking_lot", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", +] + +[[package]] +name = "gpui-fast-macros" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e545c497ffdad0afd840525e45707d522ab35d88f231d8f657ae1b331089d3b" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-fast-platform" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27afaeacefaeeb7b3c509a6447375afa13dbf17f9fa4a99c534d6bad59b48b47" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "gpui-fast", + "gpui-fast-linux", + "gpui-fast-macos", + "gpui-fast-web", + "gpui-fast-wgpu", + "gpui-fast-windows", + "gpui-pre-ios", +] + +[[package]] +name = "gpui-fast-web" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cebce373802858838045347792f4aa634591a73739582664ac2019a0348bbff1" +dependencies = [ + "anyhow", + "async-channel", + "console_error_panic_hook", + "futures", + "gpui-fast", + "gpui-fast-wgpu", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui-fast-wgpu" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c02b5c6805781c8978d568df0bd62845a728bc9ac176ec8c4337b65a5daf41f" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui-fast", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui-fast-windows" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e07b0bad5ff42b905882f91797540299a6747a1a34d07696083e3be3bb580c67" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "dunce", + "etagere", + "futures", + "gpui-fast", + "gpui-pre-collections", + "gpui-pre-scheduler", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-kit" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aca1dedfea73645b60b5893c9c16de0b1396f2cf4251d19e5f03a295c1b61879" +dependencies = [ + "gpui-base", + "gpui-component", + "gpui-fast", + "gpui-fast-platform", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-platform", + "gpui-pre-web", +] + +[[package]] +name = "gpui-kit-assets" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5abe0bbd8f9226170c8726e529f6574519e297e9ed1bde86d5fd4d3536079a" +dependencies = [ + "anyhow", + "gpui-pre", + "gpui-pre-reqwest", + "log", + "rust-embed", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "gpui-pre" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29db89613527079737f53752eab27fb65c9a7dd94979256c65a0e590c9fd34d8" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bitflags 2.13.2", + "chrono", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-pre-bench-metrics", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "heapless", + "icu_segmenter", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "taffy", + "thiserror 2.0.21", + "ttf-parser", + "unicode-segmentation", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-scap", +] + +[[package]] +name = "gpui-pre-apple" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a460605ec4e8cbcc9b213462a84b1d6cc3efe4ed8f0e18669659f07b2d5c6aec" +dependencies = [ + "anyhow", + "async-task", + "block2 0.6.2", + "cbindgen", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "dispatch2", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "libc", + "log", + "mach2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-security", + "parking_lot", + "pathfinder_geometry", + "smallvec", + "thiserror 2.0.21", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-bench-metrics" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6a8ff719fdc3da4f5f497b5d39c5545634424073f94943b003c818fc95e1d5" +dependencies = [ + "anyhow", + "criterion", + "libc", + "perf-event2", +] + +[[package]] +name = "gpui-pre-collections" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb60ece401b399fd5ec13911915b407b0c6efa1f6c9a9f66201aaac56c7ec39d" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb2a3f5e6c9f300f432ae7989a81b262953ccfb3f9385be51cdc3303aef8858" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55245de7e3b6e219596fd98bb29555d6e3fe44c055d025960affeb11b894c048" +dependencies = [ + "anyhow", + "bytes", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-ios" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10656f6db4bbedb60bd7fcbb8a70f7c7eeee45bc4582fb8933ca807d2ddf52ad" +dependencies = [ + "anyhow", + "block2 0.6.2", + "futures", + "gpui-pre", + "gpui-pre-apple", + "log", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-ui-kit", + "parking_lot", + "raw-window-handle", + "uuid", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54bab418d0b7c027af9495f278619b46887362b3bd6446e246c87c5b0fa167df" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-pre-macos" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de7d3f4ad51ac387e822495db363fcc1188d349793365731d054444c4cb06ee" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-scheduler", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", + "parking_lot", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", +] + +[[package]] +name = "gpui-pre-macros" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7f67904eced61ed42ae3320a9b4b388aed893f8547fffe9161ec425ad162eb" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b785bdcbd229f762096aebf4f9ae913bdaa9db49794907cabaf516eb55227b" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "gpui-pre", + "gpui-pre-ios", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-wgpu", + "gpui-pre-windows", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d197569e3b1fd84170f574bb866e95ecbc2625e7ae9443c877150eaf7d53374" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d3fa97eeb4ea233635b756684affe96db9e29fc2af8cdc269268bd7ff649fc" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c2ed1e44195079d4461b83a99845aa9a52ce8fcf1516410816670a03d32d94" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui-pre-sum-tree" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6085e61458366decd21491396b0b90b3adf928f2ec27e170c4d3c9fba455d536" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ead57ff96b5a34e71d64758080347066d667e0c4bd4ff71e85f3a50643fa3e" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85e775c6874257c63f9cf51d99f0aba13b3bc615ed8d47e7d5338e3b5941f6a" +dependencies = [ + "anyhow", + "async-channel", + "console_error_panic_hook", + "futures", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui-pre-wgpu" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc76608d385f6d0dcbad44504b1bf669dac3b7d7ffa2885acdb7c1c358caf852" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-windows" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7201762420222d5b5ce5bdf0056996199212a3349bfb2457a3a0238c688681f3" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "dunce", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-scheduler", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1043bbe8d711baf03e774b03bae6521155f017450b866dcaa987a07d978d660a" +dependencies = [ + "gpui-pre-ztracing-macro", + "tracing", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85701631a9bbecf47e6d19f145e85e57767e7c50f7192d5b1bf65b5ef43cb373" + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hack-desktop" +version = "0.0.0" +dependencies = [ + "gpui-kit", + "hack-desktop-core", + "image", +] + +[[package]] +name = "hack-desktop-core" +version = "0.0.0" +dependencies = [ + "libc", + "serde", + "serde_json", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484be5657551d7868d67587df967fb4fcbe9d266a761cc855aaa148fda0b3cc7" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "tiff", + "zune-core 0.5.3", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" +dependencies = [ + "bitflags 2.13.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a6b9d4e8b48acd531fbf1241ad9b270df6e60b6b92bdea1469f4f40899d805" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82dd26aa4dc0b5b70b9115de59824842bcb43cf4a28e1b1e6d0ad148c93f93e" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd4872650e70dc907dd4f7ebc449d7d657a0d2658e423b2e724970870bbbf75b" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.5", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5848f5a9e8117cf94f2f90faac24a87ff81600ff8628c1e3deaab572e76bfa1" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20521a903f02b0a573cd6840e9d0a5a0cc043d361fe924092cd70f8556cc9c4a" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8a68be21d0755f83bfe47e79b896cc732687cf1903f0e50585e8d6ecc53076" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05e0c3c3e4edaa21d87c645852ed0b0c3663752bcc6ec43353bd6189dce35e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "target-features", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0dd362bf26c0ffdd143df6da66a25d287e196eff482f117771010b207ad163" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14474170feb673cfb18b3565111a9869797cc1d290fb9ec969c53f5e1e231c6d" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "normpath" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "8.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41a04a5f59df9322cc657286dd2d79f90816119b22c5f847607c850110205ab" +dependencies = [ + "bitflags 2.13.2", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "objc2-core-foundation", + "objc2-core-services", + "walkdir", + "windows-sys 0.61.2", +] + +[[package]] +name = "notify-rust" +version = "4.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da5b5d3b7809bf71858da5a2818c3404ca4688c02c8abecdda0fcdec235df3" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus 6.3.0", +] + +[[package]] +name = "notify-types" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c491a384baf9fec20df565cdb8138a12809d4c7439763b566eb8114c5d59979" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.5", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-uniform-type-identifiers", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7902ac02859fc1f7045f8b598c63f1ae0cc7efeaa06a9bc9f3d9a3c955974fa4" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest 0.10.7", + "endi", + "futures-lite", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zbus 5.19.0", + "zbus_macros 5.19.0", + "zeroize", + "zvariant", +] + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "open" +version = "5.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f59006ce1e204c30956638535b74d317bca99477b9356dfaec54c37f6f600ab" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "perf-event-data" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1" +dependencies = [ + "bitflags 2.13.2", + "c-enum", + "perf-event-open-sys2", +] + +[[package]] +name = "perf-event-open-sys2" +version = "5.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c25955321465255e437600b54296983fab1feac2cd0c38958adeb26dbae49e" +dependencies = [ + "libc", + "memoffset", +] + +[[package]] +name = "perf-event2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f" +dependencies = [ + "bitflags 2.13.2", + "c-enum", + "libc", + "memmap2", + "perf-event-data", + "perf-event-open-sys2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "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", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.8", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.19+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proptest" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" +dependencies = [ + "bit-set 0.11.1", + "bit-vec 0.10.2", + "bitflags 2.13.2", + "chacha20", + "core_detect", + "num-traits", + "proptest-macro", + "rand 0.10.3", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99782ea31698c2206c6887c1220d2f594b291c26cb9ec5b6d7cf175320bc212" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.21", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.7", + "once_cell", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif 0.14.2", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.16.2", + "tiny-skia", + "usvg 0.46.0", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ropey" +version = "2.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74" +dependencies = [ + "str_indices", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] + +[[package]] +name = "rust-embed" +version = "8.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98f580cf7a2863aeb6827800283f52cbe6fb3f3bb7c1843783f903c16dcb8ad" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93e2f69cf224a6c7df7509aa5ba4a23f56828ee41b040e92ac8762371eb9ba7" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915e33dccbe7deb2b8a342dc4c517503fe85e0230ddbae40a4fb0220493dad9b" +dependencies = [ + "globset", + "sha2 0.11.1", + "walkdir", +] + +[[package]] +name = "rust-i18n" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8502e73e6461c738659455c5d2cf95bc132760a6f203c717e66ee72b78906d7f" +dependencies = [ + "globwalk", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54a28dd9d46108d56b9f0c85b220908844233e561c03087890325104f90f2c6" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "rust-i18n-support" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e806c2a9701cbd6043992cd935c30cb1fdb128fa15e7b506f3b46f97045737c0" +dependencies = [ + "arc-swap", + "base62", + "foldhash", + "globwalk", + "itertools 0.11.0", + "normpath", + "serde", + "serde-saphyr", + "serde_json", + "siphasher", + "toml 0.8.23", + "triomphe", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102f35f7de7b06d8c61f1cff6592f5253e0dea431c74867aad5dfd70f44663fb" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a0760a887c14823fca5a1c970b01d47c328c696c0c64829d1df50e8f853fd5" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.10", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c187e0a369471c550af028cba56d4f4fa2a489ad28151ae95a4d340a2a8b782" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs 6.0.0", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149309498a5f7334b12ddae0cad8aba81ce785fe91755068867e017cf98fee46" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smol_str" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb7109d23f319619d4cc0de5464613c8b7b1a09db2774a58101459cf6be2f10" +dependencies = [ + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "svgtypes" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f12b12995e3638c70618c1facb1ce65302f6a6f706b675f07c58a5d7391120e" +dependencies = [ + "kurbo 0.13.1", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f7bd2d97198df05186f93a3a3c306845f40e8b3c22bf9279cdd7e5cf5fea7d" +dependencies = [ + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "target-features" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" + +[[package]] +name = "tauri-winrt-notification" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36" +dependencies = [ + "thiserror 2.0.21", + "windows 0.62.2", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.2.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5755c51863cdac711f1956baf8eb9588e060755f7d3c4c979e70ea1e1f32cd7" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.3", + "toml_datetime 1.1.3+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.5", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.19+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218" +dependencies = [ + "indexmap", + "toml_datetime 1.1.3+spec-1.1.0", + "toml_parser", + "winnow 1.0.5", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.5", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "triomphe" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d15f4f5898bcfc5d8a8e45ed30b2f108d710c1a2e5489dbf80e078f707181bc" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993ad287eb20e8ba755312801cfe1413e0f25d604075863acaddca10b04c1833" + +[[package]] +name = "unicode-script" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa04c955db05429d464c74d495c1ee677e1866e783a4c3747f9c0cef7756cabf" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.14.0", + "kurbo 0.13.1", + "log", + "pico-args", + "roxmltree 0.21.2", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.16.2", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "uuid" +version = "1.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed99139f7dac9b5cfe5f17be4d66dc7079a34dee1d74bcc77217119240e267c" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.10", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm_thread" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +dependencies = [ + "futures", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "log", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad661da2ee99f208c30090b585284b2ab5b8bf2358ddf2d2215592808ec62f1a" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bit-vec 0.9.1", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3f2f24aee039ffe9ef527300daf6b7428c58edeefdaa1700f4fa0fd754df16" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ccf093bd215ae9dc4f96df293e675513ac1f2569f8293a2911cc34ca3505d1" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b20f43aeb9962d05e61e3ccb717e2e27ba1b04e979d4ca7e361384f8ee03db56" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1331b08fa2362c5d454eccc4448e6e4f01eccfea569a00e310adbfc4415f2bdc" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a57452fe453658e141aafdb09de1028afa498805fab518238ddf372a518813ff" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.10.0", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "ndk-sys", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44a62ead68aad7cd40ac9e2cd2b8ec64f32dfbad2765f429b9fb74b8b735869f" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f6ee0bd50f5ab79fb8c25e3f33afb27126f034af4ba5909c7a0967058659e6" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.21", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.2", + "libc", + "quick-xml", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac366ea532fa8ee6a992cec9dea525b1dc284e69702f36c2ab5fbebadc81ec86" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.5", + "zbus_macros 5.19.0", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4aae740b23927ceabed962431abab2cdefb9f7f885e5bbab062da09bf580e4" +dependencies = [ + "async-trait", + "bitflags 2.13.2", + "endi", + "futures-core", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "socket2", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.5", + "zbus_macros 6.3.0", + "zbus_utils", + "zcheapstr", + "zruntime", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_macros" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c9dd4beaf145c16fcb2902862f50da86e54aed13dd368ddf64a1d17e6b8529" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "zbus_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" +dependencies = [ + "serde", + "winnow 1.0.5", + "zvariant", +] + +[[package]] +name = "zbus_utils" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af55e52da295311b368c45bcae221f020e28e97a2420360170ce389218e76ec7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.10", + "winnow 1.0.5", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.5", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939f186d9ecebe044315a5cb6d6f78568bbc21152adc954d5a0c2f58bdbc9920" +dependencies = [ + "serde", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.8", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zruntime" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6a55d4f521760475a30f7b96779508fe8c40dc834478709454dd1d5f0ebc72" +dependencies = [ + "futures-core", + "futures-io", + "rustix", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.3", +] + +[[package]] +name = "zvariant" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow 1.0.5", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.10", + "winnow 1.0.5", +] diff --git a/apps/desktop/Cargo.toml b/apps/desktop/Cargo.toml new file mode 100644 index 000000000..662da4c86 --- /dev/null +++ b/apps/desktop/Cargo.toml @@ -0,0 +1,23 @@ +# Separate workspace for the Hack desktop app (HACK-2214). It has its own +# lockfile and target directory so it never changes the build, lockfile or +# release of packages/runtime-core or packages/config-compiler. +[workspace] +resolver = "3" +members = ["core", "app"] + +[workspace.package] +version = "0.0.0" +edition = "2024" +rust-version = "1.98.1" +license = "MIT" +publish = false + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +gpui-kit = { version = "=0.9.1" } +hack-desktop-core = { path = "core" } + +[profile.release] +strip = true +lto = "thin" diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 5d1e14eff..1f499e2e6 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -42,8 +42,8 @@ detail pane, status bar. Light and dark follow the system | Loading | First read in flight | Three skeleton rows | none | | Loaded | `projects[]` non-empty | Groups Running / Stopped; dot per row (green running, amber degraded, grey stopped, red failed or needs attention, grey "?" unknown); "always on" mark | Select row | | Empty | Registry has no projects | "No projects yet" + `cd your-repo && hack init` | Copy command, open docs | -| Daemon not running | `/v1/status` unreachable | List loads through the CLI; banner "hackd is off: status refreshes slowly" | Start hackd | -| Daemon other version | `/v1/status.version` ≠ CLI version | CLI fallback; banner "hackd vX, CLI vY" | Restart hackd | +| Daemon not running | `/v1/status` unreachable | Unknown; last good list kept stale. Fallback needs R18: today's CLI listing writes the registry. Retry every 10 s. | Start hackd | +| Daemon other version | `/v1/status.version` ≠ CLI version | Unknown; fallback needs R18. Last good list kept stale. Retry every 10 s. | Restart hackd | | Docker not reachable | `runtime_ok: false` (Docker observation only) | Compose rows keep names, dots "?"; native rows keep their own state from native `ps`; banner with `runtime_error` | Open Docker help | | CLI missing or too old | `hack` not on PATH, or outside the supported range | Full-pane card instead of the list; status bar shows no engine/hackd state | Copy install command | | Error | CLI exit ≠ 0 or bad JSON | Banner with `code` and `message`; last good list kept, marked stale with its age | Retry, copy error | @@ -83,7 +83,7 @@ Header: name, state pill (+ uptime when known), backend pill, URL, buttons, | Starting | this app's `up` in flight, or native `pending` | blue "Starting…" | none (no cancel in v0) | | Running | every defined service has a running container | green "Running · 2 hours" (from Docker status text) | Open, Restart, Stop | | Running, some exited (0) | the rest run; some exited 0 and their completion role is unknown | green "Running · migrate exited (0)" with tooltip "completion role unknown" | Open, Restart, Stop | -| Degraded | some running; a long-running service down, exited ≠ 0, or `(unhealthy)` | amber "Degraded · worker down" | Open, Restart, Stop | +| Degraded | some running; a service with a container that is down, exited ≠ 0, or `(unhealthy)`. A defined service with no container is not counted: it may be in an inactive Compose profile (expected services need R2) | amber "Degraded · worker down" | Open, Restart, Stop | | Stopping | this app's `down` in flight | grey "Stopping…" | none | | Failed start | last `up` reported `services.failed`, or nothing runs and a service exited ≠ 0 | red "Failed to start: api, worker" | Start (retry), Stop, logs filtered to those services | | Needs attention | always-on gave up (R12b), `status: missing`, or an `input_diagnostic` that is not the native one | red "Needs attention" + one line why | the one fix | @@ -281,7 +281,7 @@ Fields and commands needed from the CLI side: ## 7. Data contract Polling stops while the window is hidden. Every CLI call has a timeout and is -killed with its process group on expiry; a timeout makes the screen Unknown. +stopped on expiry: SIGTERM to its process group (the CLI forwards it to the subprocesses it owns), then SIGKILL after 15 s; a timeout makes the screen Unknown. Exit codes: a valid JSON payload is parsed even when the exit code is not 0, because the unhealthy states live there. @@ -298,7 +298,7 @@ because the unhealthy states live there. | Screen | Reads | How often | Timeout | |---|---|---|---| -| Sidebar, header | `GET /v1/projects` if `/v1/status.version` matches the CLI, else `hack projects --json --no-daemon`; native rows add `HACK_RUNTIME_BACKEND=native hack ps --json --path` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | +| Sidebar, header | `GET /v1/projects` if `/v1/status.version` matches the CLI; otherwise Unknown until R18 provides observe-only CLI listing. Native rows add `HACK_RUNTIME_BACKEND=native hack ps --json --path` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | | Services CPU/memory | `hack usage --json --details` (until R3) | 5 s while visible | 10 s | | Logs | `hack logs --json --path [] --tail 500` (one long-running child) | stream | killed on tab change | | Env | `hack env list --json --path ` | on tab open, after env actions | 10 s | @@ -306,9 +306,9 @@ because the unhealthy states live there. | Doctor, onboarding | `hack doctor --json` (per project and once outside), `hack daemon status --json` | on open; every 10 min | 60 s | | Daemon | `GET /v1/status` | 10 s | 1 s | | Upgrades | R14 daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | -| Actions | `hack up -d --json`, `down --json`, `restart --json`, `open --json`, `daemon start\|restart\|install` (exit code), `autostart …` | on click | 10 min for `up`, 2 min others | +| Actions | `hack up -d --json`, `down --json`, `restart --json`, `open --json`, `daemon start\|restart\|install` (exit code), `autostart …` | on click | `up`/`down`/`restart`: none; the app waits for the CLI, which owns their timeouts (after 6 h it only shows a warning), 2 min others | -The 23 numbered requests (R1–R17) this design needs from the CLI and daemon are in +The 24 numbered requests (R1–R18) this design needs from the CLI and daemon are in [Appendix C](#appendix-c-requests-to-the-cli-and-daemon). ## 8. Scope @@ -390,3 +390,4 @@ Each is small enough for one PR. 21. **R15** `--json` for `hack daemon start|stop|restart|install`. 22. **R16** Trust status: a doctor check that reports whether the system trust store (and NSS on Linux) trusts the local CA, or `unknown`. 23. **R17** `kind: safe | admin | manual` on #790's findings and plan steps, so the app knows which to offer as Run. +24. **R18** Observe-only project listing, including registered native projects: no auto-registration, `lastSeenAt` updates or registry writes. Until it exists, the desktop refuses CLI listing fallback and reports Unknown when a matching hackd snapshot is unavailable. The daemon-off screen names `hack daemon start` and offers Start hackd as its first recovery action. diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 000000000..03fe99700 --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,83 @@ +# Hack desktop app (preview) + +Unsupported preview of a minimal native window for Hack (HACK-2214): every +project, its state and services, and Start / Stop / Restart / Open. It is a view +over the `hack` CLI and the `hackd` socket; it keeps no state beyond window +layout, holds no credentials and runs `hack` as you for every action. Not part +of the release or onboarding yet. + +| Crate | What | +|---|---| +| `core` (`hack-desktop-core`) | Models, CLI runner with timeouts, read-only hackd client, project state, list/detail view models, fixtures. No GUI dependency. | +| `app` (`hack-desktop`) | The window, built with [gpui-kit](https://gpui-kit.com/) `=0.9.1`. | + +This is its own Cargo workspace with its own lockfile and target directory; it +does not touch `packages/runtime-core`. + +## Build and run + +Rust **1.98.1**, pinned for this workspace only by `rust-toolchain.toml` (rustup) +and `mise.toml` (mise); the repo root keeps its own pin. macOS needs at least +1.98 (`gpui-pre-macos` uses `Option::map_or_default`). macOS 15+, or Linux with +Wayland or X11 and Vulkan. Linux build packages: `clang libfontconfig-dev +libwayland-dev libxkbcommon-x11-dev libx11-xcb-dev libssl-dev libzstd-dev +libvulkan-dev`. + +```sh +cd apps/desktop +cargo test -p hack-desktop-core # no daemon, Docker or Hack home needed +cargo run -p hack-desktop # live: matching hackd; otherwise needs R18 +cargo run -p hack-desktop -- --list-fixtures +cargo run -p hack-desktop -- --fixtures failed-start --theme dark +``` + +## Check the live data path without a window + +```sh +cargo run -p hack-desktop-core --bin hack-desktop-state # all projects +cargo run -p hack-desktop-core --bin hack-desktop-state -- --project +``` + +`hack-desktop-state` reads exactly what the window reads: hackd when its version +matches the CLI, plus native `hack ps` for native projects. Today's CLI listing +auto-registers runtime projects and writes the registry, so it is never used as +a fallback. Without a matching daemon the reader reports Unknown / needs R18 +(observe-only projects), rather than writing or claiming no projects exist. +The window retains stale names with Unknown states and disables project actions. +It offers **Start hackd** when unreachable or **Restart hackd** for a version +mismatch. Only a fresh matching snapshot restores observed states. +It applies that to the same view model and prints the list and +each project's detail as JSON. It runs no action, opens no window and writes +nothing. + +An accepted refresh that changes the selected project root or backend clears the +previous log tail, including when Follow is off. + +Live reads refresh every two seconds with a matching daemon and retry every ten +seconds when unavailable. Every request has an order token: a read started before +an action finishes cannot clear its failure, and an older result cannot replace a +newer one. Restarting/dead containers are failures; created/paused containers +without a running service remain Unknown, even when the CLI summary says stopped. + +Every designed state has a fixture (`loading`, `empty`, `daemon-down`, `many-projects`, +`daemon-mismatch`, `engine-down`, `error`, `cli-missing`, `stopped`, `starting`, +`running`, `degraded`, `stopping`, `failed-start`, `needs-attention`). + +## Screenshots without a display + +```sh +cargo run -p hack-desktop --features screenshots -- --screenshots /tmp/shots +``` + +This renders every fixture in light and dark to PNG with GPUI's offscreen +renderer (WGPU on Linux, Metal on macOS). It opens no window. Current renders +(all 15 fixtures, row by row: loading, empty, daemon-down / daemon-mismatch, +engine-down, error / cli-missing, many-projects, stopped / starting, running, +degraded / stopping, failed-start, needs-attention): +[light](screenshots/fixtures-light.png), [dark](screenshots/fixtures-dark.png). + +## What v0 does not do yet + +Logs, Env and Activity tabs, CPU and memory, Doctor view, onboarding, always-on, +daemon actions and upgrades come in later units. The design and the CLI/daemon +requests they need are in `DESIGN.md` (PR #800). diff --git a/apps/desktop/app/Cargo.toml b/apps/desktop/app/Cargo.toml new file mode 100644 index 000000000..36f9b886a --- /dev/null +++ b/apps/desktop/app/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "hack-desktop" +description = "Minimal native desktop app for Hack (unsupported preview)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "hack-desktop" +path = "src/main.rs" + +[features] +# Offscreen rendering of fixture states to PNG (used for design evidence). +screenshots = ["gpui-kit/test-support", "dep:image"] + +[dependencies] +hack-desktop-core.workspace = true +gpui-kit.workspace = true +image = { version = "0.25", optional = true, default-features = false, features = ["png"] } diff --git a/apps/desktop/app/src/main.rs b/apps/desktop/app/src/main.rs new file mode 100644 index 000000000..52639bb3b --- /dev/null +++ b/apps/desktop/app/src/main.rs @@ -0,0 +1,129 @@ +//! Hack desktop app (unsupported preview, HACK-2214). +//! +//! ```text +//! hack-desktop live: hackd + `hack ... --json` +//! hack-desktop --fixtures running canned data for one designed state +//! hack-desktop --list-fixtures +//! hack-desktop --theme light|dark default: follow the system +//! hack-desktop --screenshots (feature `screenshots`) render every +//! fixture, light and dark, offscreen +//! ``` + +#[cfg(feature = "screenshots")] +mod screenshots; +mod view; + +use std::sync::Arc; + +use gpui_kit::component::{Theme, ThemeMode}; +use gpui_kit::{AppContext as _, Bounds, WindowBounds, WindowOptions, px, size}; +use hack_desktop_core::fixtures::{self, Fixture}; +use hack_desktop_core::source::{FixtureSource, LiveSource, Source}; + +pub(crate) struct Args { + pub fixture: Option, + pub theme: Option, + pub screenshots: Option, +} + +fn usage() -> ! { + eprintln!( + "usage: hack-desktop [--fixtures ] [--theme light|dark] [--list-fixtures] [--screenshots ]" + ); + std::process::exit(2) +} + +fn parse_args() -> Args { + let mut a = Args { + fixture: None, + theme: None, + screenshots: None, + }; + let mut it = std::env::args().skip(1); + while let Some(arg) = it.next() { + match arg.as_str() { + "--fixtures" => { + let name = it.next().unwrap_or_else(|| usage()); + a.fixture = Some(Fixture::parse(&name).unwrap_or_else(|| { + eprintln!("unknown fixture {name:?}; see --list-fixtures"); + std::process::exit(2) + })); + } + "--list-fixtures" => { + for f in Fixture::ALL { + println!("{}", f.name()); + } + std::process::exit(0) + } + "--theme" => { + a.theme = Some(match it.next().as_deref() { + Some("light") => ThemeMode::Light, + Some("dark") => ThemeMode::Dark, + _ => usage(), + }); + } + "--screenshots" => a.screenshots = Some(it.next().unwrap_or_else(|| usage()).into()), + "-h" | "--help" => usage(), + _ => usage(), + } + } + a +} + +pub(crate) fn source_for(fixture: Option) -> Arc { + match fixture { + Some(f) => Arc::new(FixtureSource(f)), + None => Arc::new(LiveSource::from_env()), + } +} + +pub(crate) fn window_options() -> WindowOptions { + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds { + origin: Default::default(), + size: size(px(1040.), px(680.)), + })), + app_id: Some("dance.hack.desktop".into()), + ..WindowOptions::default() + } +} + +fn main() { + let args = parse_args(); + #[cfg(feature = "screenshots")] + if let Some(dir) = &args.screenshots { + screenshots::run(dir); + return; + } + #[cfg(not(feature = "screenshots"))] + if args.screenshots.is_some() { + eprintln!("--screenshots needs a build with --features screenshots"); + std::process::exit(2); + } + gpui_kit::application().run(move |cx| { + gpui_kit::init(cx); + let fixture = args.fixture; + let theme = args.theme; + let source = source_for(fixture); + gpui_kit::open_window(window_options(), cx, move |window, cx| { + match theme { + Some(mode) => Theme::change(mode, Some(window), cx), + None => Theme::sync_system_appearance(Some(window), cx), + } + cx.new(|cx| { + let mut app = view::HackApp::new(source, fixture.is_none(), cx); + if let Some(f) = fixture { + app.select(fixtures::selected(f)); + if matches!(f, Fixture::DaemonDown | Fixture::DaemonMismatch) { + app.simulate_daemon_unavailable(f == Fixture::DaemonMismatch); + } + } + if let Some(a) = fixture.and_then(|f| fixtures::local(f).in_flight) { + app.simulate_in_flight(a); + } + app + }) + }) + .expect("failed to open the window"); + }); +} diff --git a/apps/desktop/app/src/screenshots.rs b/apps/desktop/app/src/screenshots.rs new file mode 100644 index 000000000..6c5b9b915 --- /dev/null +++ b/apps/desktop/app/src/screenshots.rs @@ -0,0 +1,67 @@ +//! Offscreen rendering of every fixture state, light and dark, to PNG. +//! +//! Uses GPUI's headless renderer (WGPU on Linux, Metal on macOS). No window +//! is shown and no display server is needed beyond a Vulkan device (a +//! software one such as lavapipe works). + +use std::path::Path; +use std::sync::Arc; + +use gpui_kit::component::{Theme, ThemeMode}; +use gpui_kit::test::TestWindowExt as _; +use gpui_kit::{AppContext as _, HeadlessAppContext, WindowOptions}; +use hack_desktop_core::fixtures::{self, Fixture}; + +use crate::view::HackApp; + +pub fn run(dir: &Path) { + std::fs::create_dir_all(dir).expect("create screenshot dir"); + let mut cx = HeadlessAppContext::with_platform( + gpui_kit::platform::current_platform(true).text_system(), + Arc::new(gpui_kit::assets::Assets), + gpui_kit::platform::current_headless_renderer, + ); + cx.update(gpui_kit::init); + for mode in [ThemeMode::Light, ThemeMode::Dark] { + for f in Fixture::ALL { + cx.update(|cx| Theme::change(mode, None, cx)); + let opts = WindowOptions { + focus: false, + show: false, + ..crate::window_options() + }; + let (handle, _) = cx + .update(|cx| { + gpui_kit::open_window(opts, cx, |_, cx| { + cx.new(|cx| { + let mut app = HackApp::new(crate::source_for(Some(f)), false, cx); + app.select(fixtures::selected(f)); + if matches!(f, Fixture::DaemonDown | Fixture::DaemonMismatch) { + app.simulate_daemon_unavailable(f == Fixture::DaemonMismatch); + } + if let Some(a) = fixtures::local(f).in_flight { + app.simulate_in_flight(a); + } + app + }) + }) + }) + .expect("open offscreen window"); + cx.update_window(handle, |_, window, cx| window.render_frame(cx)) + .expect("render"); + let img = cx + .capture_screenshot(handle) + .expect("offscreen rendering must be available"); + let suffix = if mode == ThemeMode::Dark { + "dark" + } else { + "light" + }; + let path = dir.join(format!("{}-{suffix}.png", f.name())); + img.save(&path).expect("write png"); + println!("{}", path.display()); + cx.update_window(handle, |_, window, _| window.remove_window()) + .ok(); + } + } +} diff --git a/apps/desktop/app/src/view.rs b/apps/desktop/app/src/view.rs new file mode 100644 index 000000000..05f159fc4 --- /dev/null +++ b/apps/desktop/app/src/view.rs @@ -0,0 +1,903 @@ +//! The one window: sidebar, detail pane, status bar. +//! +//! All data comes from `hack_desktop_core`; this file only lays it out and +//! forwards clicks as view-model events. Blocking work (CLI, socket) runs on +//! the background executor, never on the UI thread. + +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use gpui_kit::component::{ + ActiveTheme, Disableable as _, Sizable as _, + button::{Button, ButtonVariants as _}, + h_flex, + switch::Switch, + tab::{Tab, TabBar}, + tag::Tag, + v_flex, +}; +use gpui_kit::{ + AnyElement, Context, FontWeight, Hsla, IntoElement, ParentElement, Render, SharedString, + Styled, Window, div, prelude::*, px, +}; +use hack_desktop_core::models::LogEntry; +use hack_desktop_core::source::{ActionOutcome, DaemonRecovery, Source}; +use hack_desktop_core::state::{Action, ProjectState, ServiceView, Tone}; +use hack_desktop_core::viewmodel::{AppModel, DetailView, Event, ListView, Phase, Row}; + +const TAIL_LINES: u32 = 20; + +pub struct HackApp { + model: AppModel, + source: Arc, + tab: usize, + tail: Result, String>, + follow: bool, + live: bool, + recovery_busy: bool, + recovery_error: Option, + /// When each project's running action started, for the slow-action + /// warning. Display only: the app never terminates a lifecycle command. + action_started: std::collections::HashMap, +} + +impl HackApp { + /// `live`: poll the source. Fixtures load once and never poll. + pub fn new(source: Arc, live: bool, cx: &mut Context) -> Self { + let mut this = Self { + model: AppModel::new(), + source, + tab: 0, + tail: Ok(Vec::new()), + follow: true, + live, + recovery_busy: false, + recovery_error: None, + action_started: std::collections::HashMap::new(), + }; + if live { + this.start_polling(cx); + } else { + this.refresh_now(); + } + this + } + + /// Synchronous refresh, for fixtures and screenshots. + pub fn refresh_now(&mut self) { + let snap = self.source.snapshot(); + self.model.apply(Event::Refreshed(snap)); + self.tail = self.selected_root().map_or_else( + || Ok(Vec::new()), + |(root, native)| self.source.log_tail(&root, native, TAIL_LINES), + ); + } + + /// Fixtures: select a project by name. + pub fn select(&mut self, name: &str) { + self.model.apply(Event::Select(name.to_owned())); + self.refresh_now(); + } + + /// Fixtures: mark an action as in flight without running it. + pub fn simulate_in_flight(&mut self, action: Action) { + if let Some(name) = self.model.detail().map(|d| d.name) { + self.model.apply(Event::ActionStarted { + project: name, + action, + }); + } + } + + /// Fixture-only unavailable daemon after a previously readable snapshot. + pub fn simulate_daemon_unavailable(&mut self, mismatch: bool) { + self.model.apply(Event::Refreshed(Err( + hack_desktop_core::source::SourceError::DaemonUnavailable { + cli: "5.0.0".into(), + daemon: mismatch.then(|| "4.3.0".into()), + message: if mismatch { + "hackd v4.3.0, CLI v5.0.0: restart hackd" + } else { + "hackd is off or unreachable: start hackd" + } + .into(), + }, + ))); + self.tail = Err("Project status could not be read".into()); + } + + fn selected_root(&self) -> Option<(PathBuf, bool)> { + let r = self.model.selected_row()?; + Some((PathBuf::from(r.repo_root.as_ref()?), r.is_native())) + } + + fn start_polling(&mut self, cx: &mut Context) { + cx.spawn(async move |this, cx| { + loop { + let Ok((source, token)) = this.update(cx, |this, _| { + (this.source.clone(), this.model.begin_refresh()) + }) else { + return; + }; + let snap = cx + .background_executor() + .spawn({ + let source = source.clone(); + async move { source.snapshot() } + }) + .await; + let root = this + .update(cx, |this, cx| { + let previous = this.selected_root(); + if !this.model.finish_refresh(token, snap) { + return None; + } + if previous != this.selected_root() { + this.tail = Ok(Vec::new()); + } + cx.notify(); + this.follow.then(|| this.selected_root()).flatten() + }) + .ok() + .flatten(); + if let Some((root, native)) = root { + let requested = root.clone(); + let tail = cx + .background_executor() + .spawn(async move { source.log_tail(&root, native, TAIL_LINES) }) + .await; + let _ = this.update(cx, |this, cx| { + // Drop a tail fetched for a project no longer selected. + if this.selected_root().is_some_and(|(r, _)| r == requested) { + this.tail = tail; + cx.notify(); + } + }); + } + let delay = this + .update(cx, |this, _| this.model.poll_interval()) + .unwrap_or(Duration::from_secs(10)); + cx.background_executor().timer(delay).await; + } + }) + .detach(); + } + + fn run_action(&mut self, action: Action, cx: &mut Context) { + if self.recovery_busy { + return; + } + let Some(row) = self.model.selected_row() else { + return; + }; + let project = row.name.clone(); + let native = row.is_native(); + let Some(root) = row.repo_root.clone().map(PathBuf::from) else { + return; + }; + if !self.model.can_start(&project) { + return; + } + self.model.apply(Event::ActionStarted { + project: project.clone(), + action, + }); + self.action_started + .insert(project.clone(), std::time::Instant::now()); + cx.notify(); + if !self.live { + return; + } + let source = self.source.clone(); + cx.spawn(async move |this, cx| { + let result = cx + .background_executor() + .spawn(async move { source.act(&root, native, action) }) + .await; + let _ = this.update(cx, |this, cx| { + if let Ok(ActionOutcome::Opened(o)) = &result { + cx.open_url(&o.url); + } + this.action_started.remove(&project); + if this.model.apply(Event::ActionFinished { + project, + action, + result, + }) { + this.refresh_now_async(cx); + } + cx.notify(); + }); + }) + .detach(); + } + + fn refresh_now_async(&mut self, cx: &mut Context) { + let source = self.source.clone(); + let token = self.model.begin_refresh(); + cx.spawn(async move |this, cx| { + let snap = cx + .background_executor() + .spawn(async move { source.snapshot() }) + .await; + let _ = this.update(cx, |this, cx| { + let previous = this.selected_root(); + if this.model.finish_refresh(token, snap) && previous != this.selected_root() { + this.tail = Ok(Vec::new()); + } + cx.notify(); + }); + }) + .detach(); + } + + fn recover_daemon(&mut self, action: DaemonRecovery, cx: &mut Context) { + if self.recovery_busy || !self.live { + return; + } + self.recovery_busy = true; + self.recovery_error = None; + let source = self.source.clone(); + cx.notify(); + cx.spawn(async move |this, cx| { + let result = cx + .background_executor() + .spawn(async move { source.recover_daemon(action) }) + .await; + let _ = this.update(cx, |this, cx| { + this.recovery_busy = false; + this.recovery_error = result.err(); + // A command exit is not evidence that a compatible daemon + // is ready. Only the next ordered snapshot can restore rows. + this.refresh_now_async(cx); + cx.notify(); + }); + }) + .detach(); + } +} + +fn tone_color(tone: Tone, cx: &gpui_kit::App) -> Hsla { + let t = cx.theme().clone(); + match tone { + Tone::Ok => t.success, + Tone::Warn => t.warning, + Tone::Busy => t.info, + Tone::Error => t.danger, + Tone::Idle => t.muted_foreground, + } +} + +fn dot(color: Hsla) -> impl IntoElement { + div().size(px(8.)).rounded_full().bg(color).flex_none() +} + +fn state_tag(state: &ProjectState) -> Tag { + let tag = match state.tone() { + Tone::Ok => Tag::success(), + Tone::Warn => Tag::warning(), + Tone::Busy => Tag::info(), + Tone::Error => Tag::danger(), + Tone::Idle => Tag::secondary(), + }; + tag.small().rounded_full().child(state.label()) +} + +impl HackApp { + fn sidebar(&self, list: &ListView, cx: &mut Context) -> impl IntoElement { + let t = cx.theme().clone(); + let header = |label: &'static str| { + div() + .px_3() + .pt_3() + .pb_1() + .text_xs() + .font_weight(FontWeight::SEMIBOLD) + .text_color(t.muted_foreground) + .child(label) + }; + let mut col = v_flex() + .w(px(232.)) + .h_full() + .flex_none() + .bg(t.sidebar) + .border_r_1() + .border_color(t.sidebar_border) + .text_color(t.sidebar_foreground) + .text_sm(); + col = col.child( + div() + .px_3() + .py_3() + .font_weight(FontWeight::BOLD) + .child("Hack"), + ); + // Project groups scroll; the header above and the machine rows below + // stay fixed, so every project stays reachable however many there are. + let mut body = v_flex() + .id("project-list") + .flex_1() + .min_h_0() + .overflow_y_scroll(); + match &list.phase { + Phase::Loading => { + for i in 0..3usize { + body = body.child( + div() + .id(("skeleton", i)) + .mx_3() + .my_1() + .h(px(20.)) + .rounded_md() + .bg(t.muted), + ); + } + } + Phase::Empty | Phase::CliMissing | Phase::Error { .. } => {} + Phase::Ready => { + if !list.running.is_empty() { + body = body.child(header("RUNNING")); + for r in &list.running { + body = body.child(self.project_row(r, cx)); + } + } + if !list.stopped.is_empty() { + let unknown = list + .stopped + .iter() + .filter(|r| matches!(r.state, ProjectState::Unknown { .. })) + .count(); + body = body.child(header(if unknown == list.stopped.len() { + "UNKNOWN" + } else if unknown > 0 { + "STOPPED / UNKNOWN" + } else { + "STOPPED" + })); + for r in &list.stopped { + body = body.child(self.project_row(r, cx)); + } + } + } + } + let t = cx.theme().clone(); + col.child(body).child( + v_flex() + .border_t_1() + .border_color(t.sidebar_border) + .py_2() + // v0 does not read routing status yet: shown as unknown. + .child(machine_row("Routing and DNS", None, t.muted_foreground)) + .child(machine_row( + "Doctor", + (list.doctor_count > 0).then_some(list.doctor_count), + // Unknown until Doctor has actually reported. + match (list.doctor_known, list.doctor_count) { + (false, _) => t.muted_foreground, + (true, 0) => t.success, + (true, _) => t.warning, + }, + )), + ) + } + + fn project_row(&self, r: &Row, cx: &mut Context) -> AnyElement { + let t = cx.theme().clone(); + let name = r.name.clone(); + h_flex() + .id(SharedString::from(format!("project-{}", r.name))) + .mx_2() + .px_2() + .py_1() + .gap_2() + .rounded_md() + .when(r.selected, |d| { + d.bg(t.sidebar_accent) + .text_color(t.sidebar_accent_foreground) + }) + .hover(|d| d.bg(t.sidebar_accent)) + .cursor_pointer() + .child(dot(tone_color(r.state.tone(), cx))) + .child(div().flex_1().truncate().child(r.name.clone())) + .when(r.always_on, |d| { + d.child( + div() + .text_xs() + .text_color(t.muted_foreground) + .child("always on"), + ) + }) + .on_click(cx.listener(move |this, _, _, cx| { + this.model.apply(Event::Select(name.clone())); + this.tail = Ok(Vec::new()); + if !this.live { + this.refresh_now(); + } + cx.notify(); + })) + .into_any_element() + } + + fn banners(&self, list: &ListView, cx: &mut Context) -> impl IntoElement { + let t = cx.theme().clone(); + let mut col = v_flex(); + for b in &list.banners { + col = col.child( + div() + .px_4() + .py_1() + .text_xs() + .bg(t.warning.opacity(0.15)) + .border_b_1() + .border_color(t.border) + .child(b.text()), + ); + } + if let Some(action) = self.model.daemon_recovery() { + col = col.child( + h_flex() + .px_4() + .py_2() + .gap_3() + .child( + Button::new("recover-hackd") + .label(action.label()) + .small() + .disabled(self.recovery_busy) + .loading(self.recovery_busy) + .on_click( + cx.listener(move |this, _, _, cx| this.recover_daemon(action, cx)), + ), + ) + .children( + self.recovery_error + .clone() + .map(|e| div().text_sm().text_color(t.danger).child(e)), + ), + ); + } + col + } + + fn detail(&self, d: &DetailView, cx: &mut Context) -> impl IntoElement { + let t = cx.theme().clone(); + let mut buttons = h_flex().gap_2(); + for a in [Action::Open, Action::Restart, Action::Stop, Action::Start] { + if !d.actions.contains(&a) { + continue; + } + let mut b = Button::new(a.label()) + .label(a.label()) + .small() + .disabled(d.busy || self.recovery_busy); + b = match a { + Action::Start => b.primary(), + Action::Stop => b.outline(), + _ => b.outline(), + }; + buttons = buttons.child(b.on_click(cx.listener(move |this, _, _, cx| { + this.run_action(a, cx); + }))); + } + let header = v_flex() + .gap_2() + .px_5() + .pt_4() + .pb_3() + .border_b_1() + .border_color(t.border) + .child( + h_flex() + .gap_3() + .child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .child(d.name.clone()), + ) + .child(state_tag(&d.state)) + .child( + Tag::secondary() + .small() + .outline() + .rounded_full() + .child(d.backend.clone()), + ) + .child(div().flex_1()) + .child(buttons), + ) + .child( + h_flex() + .gap_4() + .text_sm() + .text_color(t.muted_foreground) + .children(d.url.clone().map(|u| div().text_color(t.link).child(u))) + .child(div().flex_1()) + .child( + Switch::new("always-on") + .label("Always on") + .checked(d.always_on) + .small() + .disabled(true) + .tooltip("Needs hack autostart (HACK-2213)"), + ), + ) + .children(reason_line(&d.state, d.last_error.as_deref(), cx)) + .children( + self.slow_action_warning(&d.name) + .map(|w| div().text_sm().text_color(t.warning).child(w)), + ) + .children( + d.note + .clone() + .map(|n| div().text_sm().text_color(t.muted_foreground).child(n)), + ); + let tabs = TabBar::new("detail-tabs") + .underline() + .small() + .selected_index(self.tab) + .child(Tab::new().label("Services")) + .child(Tab::new().label("Logs").disabled(true)) + .child(Tab::new().label("Env").disabled(true)) + .child(Tab::new().label("Activity").disabled(true)) + .on_click(cx.listener(|this, ix: &usize, _, cx| { + this.tab = *ix; + cx.notify(); + })); + // The header and tabs stay fixed; the tab content scrolls within the + // remaining height so every service row and log line is reachable. + v_flex() + .flex_1() + .min_w_0() + .min_h_0() + .child(header) + .child(div().px_5().pt_2().child(tabs)) + .child( + v_flex() + .id("detail-body") + .flex_1() + .min_h_0() + .overflow_y_scroll() + .child(services_table(&d.services, cx)) + .child(self.log_tail(cx)), + ) + } + + /// Shown, never acted on, once an action has run past + /// `LiveSource::LIFECYCLE_WARN_AFTER`. + fn slow_action_warning(&self, project: &str) -> Option { + let started = self.action_started.get(project)?; + (started.elapsed() >= hack_desktop_core::source::LiveSource::LIFECYCLE_WARN_AFTER).then(|| { + "Still running after 6 hours. Hack owns this command and its timeouts; the app has not stopped it.".to_owned() + }) + } + + fn log_tail(&self, cx: &mut Context) -> impl IntoElement { + let t = cx.theme().clone(); + let mono = t.mono_font_family.clone(); + let mut lines = v_flex().font_family(mono).text_xs().gap_0p5().px_3().py_2(); + let entries: &[LogEntry] = match &self.tail { + Ok(v) => v, + Err(why) => { + // A failed read is shown as such, never as an empty log. + lines = lines.child(div().text_color(t.muted_foreground).child(why.clone())); + &[] + } + }; + if matches!(&self.tail, Ok(v) if v.is_empty()) { + lines = lines.child( + div() + .text_color(t.muted_foreground) + .child("No recent log lines"), + ); + } + for e in entries { + let error = e.level.as_deref() == Some("error"); + lines = lines.child( + h_flex() + .gap_2() + .child( + div() + .w(px(64.)) + .flex_none() + .text_color(t.muted_foreground) + .child(e.service.clone().unwrap_or_default()), + ) + .child( + div() + .flex_1() + .truncate() + .when(error, |d| d.text_color(t.danger)) + .child(e.message.clone()), + ), + ); + } + v_flex() + .mx_5() + .mb_3() + .flex_1() + .min_h(px(120.)) + .rounded_md() + .border_1() + .border_color(t.border) + .bg(t.muted.opacity(0.4)) + .child( + h_flex() + .px_3() + .py_1() + .border_b_1() + .border_color(t.border) + .text_xs() + .text_color(t.muted_foreground) + .child("Log tail") + .child(div().flex_1()) + .child( + Switch::new("follow") + .label("Follow") + .checked(self.follow) + .xsmall() + .on_change(cx.listener(|this, v: &bool, _, cx| { + this.follow = *v; + cx.notify(); + })), + ), + ) + .child(lines) + .child( + div() + .px_3() + .pb_2() + .text_xs() + .text_color(t.muted_foreground) + .child("Logs are shown as your services print them. Hack does not hide secrets in logs."), + ) + } + + fn full_pane( + &self, + title: &str, + body: String, + command: Option<&str>, + cx: &mut Context, + ) -> AnyElement { + let t = cx.theme().clone(); + v_flex() + .flex_1() + .items_center() + .justify_center() + .gap_2() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .child(title.to_owned()), + ) + .child(div().text_sm().text_color(t.muted_foreground).child(body)) + .children(command.map(|c| { + div() + .mt_2() + .px_3() + .py_1() + .rounded_md() + .bg(t.muted) + .font_family(t.mono_font_family.clone()) + .text_sm() + .child(c.to_owned()) + })) + .into_any_element() + } + + fn status_bar(&self, list: &ListView, cx: &mut Context) -> impl IntoElement { + let t = cx.theme().clone(); + let stale = list + .banners + .iter() + .any(|b| matches!(b, hack_desktop_core::viewmodel::Banner::Stale { .. })); + let engine = if !self.model.engine_observed() { + ("Docker unknown", t.muted_foreground) + } else if list + .banners + .iter() + .any(|b| matches!(b, hack_desktop_core::viewmodel::Banner::EngineDown { .. })) + { + ("Docker down", t.danger) + } else { + ("Docker ok", t.success) + }; + let daemon = if list + .banners + .iter() + .any(|b| matches!(b, hack_desktop_core::viewmodel::Banner::DaemonOff)) + { + ("hackd off", t.muted_foreground) + } else if list.banners.iter().any(|b| { + matches!( + b, + hack_desktop_core::viewmodel::Banner::DaemonMismatch { .. } + ) + }) { + ("hackd version mismatch", t.warning) + } else if stale + || list.banners.iter().any(|b| { + matches!( + b, + hack_desktop_core::viewmodel::Banner::DaemonReadFailed { .. } + ) + }) + { + ("hackd unknown", t.muted_foreground) + } else { + ("hackd on", t.success) + }; + h_flex() + .h(px(26.)) + .px_3() + .gap_4() + .text_xs() + .bg(t.status_bar) + .border_t_1() + .border_color(t.status_bar_border) + .text_color(t.muted_foreground) + .child({ + let c = list.counts; + let mut text = format!("{} running · {} stopped", c.running, c.stopped); + if c.unknown > 0 { + text.push_str(&format!(" · {} unknown", c.unknown)); + } + text + }) + // Without a snapshot the engine and hackd states are unknown. + .when(list.cli_version.is_some(), |d| { + d.child(h_flex().gap_1().child(dot(engine.1)).child(engine.0)) + .child(h_flex().gap_1().child(dot(daemon.1)).child(daemon.0)) + }) + .child(div().flex_1()) + .children(list.cli_version.clone().map(|v| format!("hack {v}"))) + } +} + +fn machine_row(label: &'static str, count: Option, color: Hsla) -> impl IntoElement { + h_flex() + .mx_2() + .px_2() + .py_1() + .gap_2() + .child(dot(color)) + .child(div().flex_1().child(label)) + .children(count.map(|n| Tag::warning().xsmall().rounded_full().child(n.to_string()))) +} + +fn reason_line( + state: &ProjectState, + error: Option<&str>, + cx: &Context, +) -> Option { + let t = cx.theme().clone(); + let text = match (state, error) { + (_, Some(e)) => format!("Last action failed: {e}"), + (ProjectState::NeedsAttention { reason } | ProjectState::Unknown { reason }, None) => { + reason.clone() + } + _ => return None, + }; + Some( + div() + .text_sm() + .text_color(t.danger) + .child(text) + .into_any_element(), + ) +} + +fn services_table(services: &[ServiceView], cx: &Context) -> impl IntoElement { + let t = cx.theme().clone(); + let cell = |w: f32| div().w(px(w)).flex_none().truncate(); + let mut table = v_flex() + .mx_5() + .my_3() + .rounded_md() + .border_1() + .border_color(t.border) + .text_sm() + .child( + h_flex() + .px_3() + .py_1() + .bg(t.table_head) + .text_color(t.table_head_foreground) + .text_xs() + .font_weight(FontWeight::SEMIBOLD) + .child(cell(140.).child("SERVICE")) + .child(cell(200.).child("STATE")) + .child(cell(64.).child("CPU")) + .child(cell(80.).child("MEMORY")) + .child(div().flex_1().child("ROUTE")), + ); + for s in services { + let color = if s.running { + t.success + } else if s.exit_code.is_some_and(|c| c != 0) { + t.danger + } else { + t.muted_foreground + }; + table = table.child( + h_flex() + .px_3() + .py_1() + .border_t_1() + .border_color(t.table_row_border) + .child(cell(140.).child(s.name.clone())) + .child(cell(200.).child(h_flex().gap_2().child(dot(color)).child(s.status.clone()))) + .child(cell(64.).text_color(t.muted_foreground).child("—")) + .child(cell(80.).text_color(t.muted_foreground).child("—")) + .child( + div() + .flex_1() + .truncate() + .text_color(t.muted_foreground) + .child(s.route.clone().unwrap_or_default()), + ), + ); + } + table +} + +impl Render for HackApp { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let list = self.model.list(); + let t = cx.theme().clone(); + let (bg, fg, font) = (t.background, t.foreground, t.font_family.clone()); + let main: AnyElement = match &list.phase { + Phase::CliMissing => self.full_pane( + "Hack CLI not found", + "The app runs the hack command for everything. Install it, then reopen the app." + .into(), + Some("brew install hack-dance/tap/hack"), + cx, + ), + Phase::Error { code, message } => self.full_pane( + "Could not read projects", + format!("{code}: {message}"), + self.model + .daemon_recovery() + .is_none() + .then_some("hack projects --json"), + cx, + ), + Phase::Empty => self.full_pane( + "No projects yet", + "Set up a repository with Hack and it appears here.".into(), + Some("cd your-repo && hack init"), + cx, + ), + Phase::Loading => self.full_pane("Loading projects…", String::new(), None, cx), + Phase::Ready => match self.model.detail() { + Some(d) => self.detail(&d, cx).into_any_element(), + None => self.full_pane("Select a project", String::new(), None, cx), + }, + }; + v_flex() + .size_full() + .bg(bg) + .text_color(fg) + .font_family(font) + .child( + h_flex() + .flex_1() + .min_h_0() + .items_start() + .child(self.sidebar(&list, cx)) + .child( + v_flex() + .flex_1() + .h_full() + .min_w_0() + .child(self.banners(&list, cx)) + .child(main), + ), + ) + .child(self.status_bar(&list, cx)) + } +} diff --git a/apps/desktop/core/Cargo.toml b/apps/desktop/core/Cargo.toml new file mode 100644 index 000000000..7eddaaa5f --- /dev/null +++ b/apps/desktop/core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "hack-desktop-core" +description = "Models, CLI runner, hackd client and view models for the Hack desktop app (no GUI)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +libc = "0.2" + +[dev-dependencies] diff --git a/apps/desktop/core/src/bin/hack-desktop-state.rs b/apps/desktop/core/src/bin/hack-desktop-state.rs new file mode 100644 index 000000000..bc2114e70 --- /dev/null +++ b/apps/desktop/core/src/bin/hack-desktop-state.rs @@ -0,0 +1,93 @@ +//! `hack-desktop-state`: a headless check of the desktop app's live data +//! path. It reads exactly what the window reads (`LiveSource::from_env` +//! then `Source::snapshot`: hackd when its version matches the CLI, +//! plus native `ps` for native projects), applies the +//! result to the same view model, and prints the list and every project's +//! detail view as JSON. +//! +//! Read-only: it runs no action, opens no window and writes nothing. +//! Without a matching daemon it reports Unknown / needs R18 rather than +//! invoking today's registry-writing `hack projects` fallback. +//! +//! ```text +//! hack-desktop-state [--project ] [--compact] +//! ``` + +use std::time::Instant; + +use hack_desktop_core::source::{LiveSource, Source}; +use hack_desktop_core::viewmodel::{AppModel, Event}; +use serde_json::{Value, json}; + +fn usage() -> ! { + eprintln!("usage: hack-desktop-state [--project ] [--compact]"); + std::process::exit(2) +} + +fn main() { + let mut project: Option = None; + let mut compact = false; + let mut args = std::env::args().skip(1); + while let Some(a) = args.next() { + match a.as_str() { + "--project" => project = Some(args.next().unwrap_or_else(|| usage())), + "--compact" => compact = true, + _ => usage(), + } + } + + let source = LiveSource::from_env(); + let started = Instant::now(); + let snap = source.snapshot(); + let read_ms = started.elapsed().as_millis(); + + let source_info = match &snap { + Ok(s) => json!({ + "ok": true, + "cli_version": s.cli_version, + "daemon": s.daemon, + "daemon_version_mismatch": s.daemon_mismatch(), + "runtime_ok": s.projects.runtime_ok, + "runtime_error": s.projects.runtime_error, + "project_count": s.projects.projects.len(), + }), + Err(e) => json!({ "ok": false, "error": format!("{e:?}") }), + }; + + let mut model = AppModel::new(); + model.apply(Event::Refreshed(snap)); + let list = model.list(); + + let names: Vec = match &project { + Some(p) => vec![p.clone()], + None => list + .running + .iter() + .chain(list.stopped.iter()) + .map(|r| r.name.clone()) + .collect(), + }; + let details: Vec = names + .iter() + .map(|name| { + model.apply(Event::Select(name.clone())); + match model.detail() { + Some(d) if d.name == *name => serde_json::to_value(d).unwrap_or(Value::Null), + _ => json!({ "name": name, "error": "no such project in the snapshot" }), + } + }) + .collect(); + + let out = json!({ + "read_ms": read_ms, + "source": source_info, + "list": list, + "details": details, + }); + let text = if compact { + serde_json::to_string(&out) + } else { + serde_json::to_string_pretty(&out) + }; + println!("{}", text.unwrap_or_default()); +} diff --git a/apps/desktop/core/src/daemon.rs b/apps/desktop/core/src/daemon.rs new file mode 100644 index 000000000..04d91dea3 --- /dev/null +++ b/apps/desktop/core/src/daemon.rs @@ -0,0 +1,452 @@ +//! Read-only client for the `hackd` Unix socket. +//! +//! Only GET requests, one per connection (`Connection: close`), with a read +//! and write timeout. The socket API has no authentication; it is owner-only +//! on disk, and the app only reads from it. A daemon whose version differs +//! from the CLI is treated as unusable for data (the CLI does the same) and +//! project reads remain unavailable until a matching daemon can be read. + +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use serde::de::DeserializeOwned; + +use crate::models::{DaemonApiStatus, ProjectsPayload}; + +const MAX_BODY: usize = 32 * 1024 * 1024; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DaemonError { + /// No socket, or nothing listening. + Unreachable(String), + Timeout, + /// The socket or its directory is not provably this user's own. + Untrusted(String), + Http { + status: u16, + body: String, + }, + BadResponse(String), +} + +impl std::fmt::Display for DaemonError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unreachable(e) => write!(f, "hackd is not reachable: {e}"), + Self::Timeout => write!(f, "hackd did not answer in time"), + Self::Untrusted(why) => write!(f, "hackd socket not trusted: {why}"), + Self::Http { status, body } => write!(f, "hackd returned {status}: {body}"), + Self::BadResponse(e) => write!(f, "unexpected response from hackd: {e}"), + } + } +} + +/// `$HACK_HOME/daemon/hackd.sock`, else `~/.hack/daemon/hackd.sock`. +#[must_use] +pub fn default_socket_path(hack_home: Option<&str>, home: Option<&str>) -> Option { + let root = match hack_home.filter(|h| Path::new(h).is_absolute()) { + Some(h) => PathBuf::from(h), + None => PathBuf::from(home?).join(".hack"), + }; + Some(root.join("daemon").join("hackd.sock")) +} + +#[derive(Debug, Clone)] +pub struct DaemonClient { + socket: PathBuf, + timeout: Duration, +} + +impl DaemonClient { + #[must_use] + pub fn new(socket: impl Into, timeout: Duration) -> Self { + Self { + socket: socket.into(), + timeout, + } + } + + /// GET a path and deserialize the JSON body. + /// + /// # Errors + /// Any [`DaemonError`]. + pub fn get_json(&self, path_and_query: &str) -> Result { + let body = self.get(path_and_query)?; + serde_json::from_slice(&body).map_err(|e| DaemonError::BadResponse(e.to_string())) + } + + /// # Errors + /// Any [`DaemonError`]. + pub fn status(&self) -> Result { + self.get_json("/v1/status") + } + + /// # Errors + /// Any [`DaemonError`]. + pub fn projects(&self) -> Result { + let v: serde_json::Value = self.get_json("/v1/projects")?; + ProjectsPayload::from_value(v).map_err(DaemonError::BadResponse) + } + + fn get(&self, path_and_query: &str) -> Result, DaemonError> { + // hackd's API has no authentication: trust it only when the socket's + // directory is this user's own and nobody else can replace entries + // in it, and the peer process runs as this user. Otherwise the app + // refuses this snapshot. + check_socket_path_len(&self.socket)?; + let uid = current_uid(); + check_private_dir(&self.socket, uid)?; + let mut s = UnixStream::connect(&self.socket) + .map_err(|e| DaemonError::Unreachable(e.to_string()))?; + match peer_uid(&s) { + Some(p) if p == uid => {} + Some(p) => { + return Err(DaemonError::Untrusted(format!( + "peer runs as uid {p}, not {uid}" + ))); + } + None => return Err(DaemonError::Untrusted("peer uid unavailable".into())), + } + s.set_read_timeout(Some(self.timeout)) + .and_then(|()| s.set_write_timeout(Some(self.timeout))) + .map_err(|e| DaemonError::Unreachable(e.to_string()))?; + let req = format!( + "GET {path_and_query} HTTP/1.1\r\nHost: hackd\r\nAccept: application/json\r\nConnection: close\r\n\r\n" + ); + s.write_all(req.as_bytes()).map_err(io_err)?; + let mut raw = Vec::new(); + s.take(MAX_BODY as u64 + 64 * 1024) + .read_to_end(&mut raw) + .map_err(io_err)?; + parse_http(&raw) + } +} + +/// Longest Unix socket path, in bytes, this platform accepts +/// (`sockaddr_un.sun_path` minus the terminating NUL). +#[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "openbsd"))] +pub const MAX_SOCKET_PATH: usize = 103; +#[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "openbsd")))] +pub const MAX_SOCKET_PATH: usize = 107; + +/// A socket path the OS cannot address (for example under a long home +/// directory on macOS) is reported clearly, without attempting a connection. +fn check_socket_path_len(socket: &Path) -> Result<(), DaemonError> { + use std::os::unix::ffi::OsStrExt; + let n = socket.as_os_str().as_bytes().len(); + if n > MAX_SOCKET_PATH { + return Err(DaemonError::Unreachable(format!( + "the hackd socket path is {n} bytes, longer than the {MAX_SOCKET_PATH} this system allows" + ))); + } + Ok(()) +} + +fn current_uid() -> u32 { + // SAFETY: getuid has no preconditions and cannot fail. + unsafe { libc::getuid() } +} + +/// The socket and its directory must be owned by `uid`, not symlinks, and +/// the directory must not be writable by group or others. +fn check_private_dir(socket: &Path, uid: u32) -> Result<(), DaemonError> { + use std::os::unix::fs::MetadataExt; + let untrusted = DaemonError::Untrusted; + let dir = socket + .parent() + .ok_or_else(|| untrusted("socket has no parent directory".into()))?; + let dm = std::fs::symlink_metadata(dir).map_err(|e| DaemonError::Unreachable(e.to_string()))?; + if !dm.is_dir() { + return Err(untrusted("daemon directory is not a directory".into())); + } + if dm.uid() != uid { + return Err(untrusted( + "daemon directory is owned by another user".into(), + )); + } + if dm.mode() & 0o022 != 0 { + return Err(untrusted( + "daemon directory is group- or world-writable".into(), + )); + } + let sm = + std::fs::symlink_metadata(socket).map_err(|e| DaemonError::Unreachable(e.to_string()))?; + if sm.file_type().is_symlink() || sm.uid() != uid { + return Err(untrusted( + "socket is a link or owned by another user".into(), + )); + } + Ok(()) +} + +#[cfg(target_os = "linux")] +fn peer_uid(s: &UnixStream) -> Option { + use std::os::fd::AsRawFd; + let mut cred = libc::ucred { + pid: 0, + uid: 0, + gid: 0, + }; + let mut len = libc::socklen_t::try_from(std::mem::size_of::()).ok()?; + // SAFETY: valid fd, out-pointer sized for ucred. + let rc = unsafe { + libc::getsockopt( + s.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_PEERCRED, + (&raw mut cred).cast(), + &raw mut len, + ) + }; + (rc == 0).then_some(cred.uid) +} + +#[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "openbsd"))] +fn peer_uid(s: &UnixStream) -> Option { + use std::os::fd::AsRawFd; + let (mut uid, mut gid) = (0, 0); + // SAFETY: valid fd and out-pointers. + let rc = unsafe { libc::getpeereid(s.as_raw_fd(), &raw mut uid, &raw mut gid) }; + (rc == 0).then_some(uid) +} + +fn io_err(e: std::io::Error) -> DaemonError { + match e.kind() { + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => DaemonError::Timeout, + _ => DaemonError::Unreachable(e.to_string()), + } +} + +/// Parse a full HTTP/1.1 response (Content-Length, chunked, or to EOF). +/// +/// # Errors +/// `BadResponse` or `Http` for a non-2xx status. +pub fn parse_http(raw: &[u8]) -> Result, DaemonError> { + let split = raw + .windows(4) + .position(|w| w == b"\r\n\r\n") + .ok_or_else(|| DaemonError::BadResponse("no header terminator".into()))?; + let head = std::str::from_utf8(&raw[..split]) + .map_err(|_| DaemonError::BadResponse("header is not UTF-8".into()))?; + let body = &raw[split + 4..]; + let mut lines = head.split("\r\n"); + let status: u16 = lines + .next() + .and_then(|l| l.split_whitespace().nth(1)) + .and_then(|c| c.parse().ok()) + .ok_or_else(|| DaemonError::BadResponse("bad status line".into()))?; + let mut chunked = false; + let mut length: Option = None; + for l in lines { + let Some((k, v)) = l.split_once(':') else { + continue; + }; + let (k, v) = (k.trim().to_ascii_lowercase(), v.trim()); + if k == "transfer-encoding" && v.eq_ignore_ascii_case("chunked") { + chunked = true; + } else if k == "content-length" { + length = v.parse().ok(); + } + } + let body = if chunked { + dechunk(body)? + } else if let Some(n) = length { + body.get(..n) + .ok_or_else(|| DaemonError::BadResponse("body shorter than Content-Length".into()))? + .to_vec() + } else { + body.to_vec() + }; + if !(200..300).contains(&status) { + return Err(DaemonError::Http { + status, + body: String::from_utf8_lossy(&body).chars().take(200).collect(), + }); + } + Ok(body) +} + +fn dechunk(mut b: &[u8]) -> Result, DaemonError> { + let bad = || DaemonError::BadResponse("bad chunked body".into()); + let mut out = Vec::new(); + loop { + let eol = b.windows(2).position(|w| w == b"\r\n").ok_or_else(bad)?; + let size_txt = std::str::from_utf8(&b[..eol]).map_err(|_| bad())?; + let size = usize::from_str_radix(size_txt.split(';').next().unwrap_or("").trim(), 16) + .map_err(|_| bad())?; + b = &b[eol + 2..]; + if size == 0 { + return Ok(out); + } + // Checked arithmetic: a huge size must be an error, not a panic. + let end = size.checked_add(2).ok_or_else(bad)?; + if size > MAX_BODY.saturating_sub(out.len()) || b.len() < end { + return Err(bad()); + } + if &b[size..end] != b"\r\n" { + return Err(bad()); + } + out.extend_from_slice(&b[..size]); + b = &b[end..]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::os::unix::net::UnixListener; + use std::thread; + + /// A short private directory for test sockets: the system temp dir can + /// be too long for a Unix socket path (macOS allows 103 bytes). + fn short_socket_dir() -> PathBuf { + use std::sync::atomic::{AtomicU32, Ordering}; + static N: AtomicU32 = AtomicU32::new(0); + let dir = PathBuf::from(format!( + "/tmp/hd{}-{}", + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir(&dir).unwrap(); + dir + } + + fn serve_once(response: Vec) -> (PathBuf, thread::JoinHandle) { + let dir = short_socket_dir(); + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap(); + } + let sock = dir.join("hackd.sock"); + let listener = UnixListener::bind(&sock).unwrap(); + let h = thread::spawn(move || { + let accepted = listener.accept(); + // Clean up first: the client may already be gone (refused or + // dropped), and a failed write must not leave the directory. + let _ = std::fs::remove_dir_all(&dir); + let Ok((mut c, _)) = accepted else { + return String::new(); + }; + let mut buf = [0u8; 4096]; + let n = c.read(&mut buf).unwrap_or(0); + let _ = c.write_all(&response); + String::from_utf8_lossy(&buf[..n]).into_owned() + }); + (sock, h) + } + + #[test] + fn reads_status_with_content_length() { + let (sock, h) = serve_once( + b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 35\r\n\r\n{\"status\":\"ok\",\"version\":\"5.0.0\"} " + .to_vec(), + ); + let s = DaemonClient::new(&sock, Duration::from_secs(2)) + .status() + .unwrap(); + assert_eq!(s.version, "5.0.0"); + let req = h.join().unwrap(); + assert!(req.starts_with("GET /v1/status HTTP/1.1\r\n")); + } + + #[test] + fn reads_chunked_projects() { + let parts = [r#"{"projects":[{""#, r#"name":"a"}],"x":1}"#]; + let mut resp = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n".to_vec(); + for p in parts { + resp.extend(format!("{:x}\r\n{p}\r\n", p.len()).into_bytes()); + } + resp.extend(b"0\r\n\r\n"); + let (sock, h) = serve_once(resp); + let p = DaemonClient::new(&sock, Duration::from_secs(2)) + .projects() + .unwrap(); + assert_eq!(p.projects[0].name, "a"); + h.join().unwrap(); + } + + #[test] + fn http_error_is_reported() { + let (sock, h) = serve_once( + b"HTTP/1.1 404 Not Found\r\nContent-Length: 21\r\n\r\n{\"error\":\"not_found\"}" + .to_vec(), + ); + let r = DaemonClient::new(&sock, Duration::from_secs(2)).status(); + assert!(matches!(r, Err(DaemonError::Http { status: 404, .. }))); + h.join().unwrap(); + } + + #[test] + fn malformed_chunks_are_errors_not_panics() { + let huge = format!( + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{:x}\r\nab\r\n0\r\n\r\n", + usize::MAX + ); + assert!(matches!( + parse_http(huge.as_bytes()), + Err(DaemonError::BadResponse(_)) + )); + let bad_delim = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nabXY0\r\n\r\n"; + assert!(matches!( + parse_http(bad_delim), + Err(DaemonError::BadResponse(_)) + )); + let short = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nff\r\nab"; + assert!(matches!( + parse_http(short), + Err(DaemonError::BadResponse(_)) + )); + } + + #[test] + fn group_writable_daemon_dir_is_not_trusted() { + use std::os::unix::fs::PermissionsExt; + let (sock, h) = serve_once(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}".to_vec()); + let dir = sock.parent().unwrap().to_path_buf(); + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o770)).unwrap(); + let r = DaemonClient::new(&sock, Duration::from_secs(2)).status(); + assert!(matches!(r, Err(DaemonError::Untrusted(_))), "{r:?}"); + // Release the test server so its thread ends. + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap(); + drop(UnixStream::connect(&sock)); + let _ = h.join(); + } + + #[test] + fn own_peer_passes_the_peer_check() { + let (a, _b) = UnixStream::pair().unwrap(); + assert_eq!(peer_uid(&a), Some(current_uid())); + } + + #[test] + fn an_over_long_socket_path_is_a_clear_error_not_a_panic() { + let long = PathBuf::from(format!("/tmp/{}/hackd.sock", "x".repeat(200))); + let r = DaemonClient::new(&long, Duration::from_millis(100)).status(); + match r { + Err(DaemonError::Unreachable(m)) => assert!(m.contains("socket path"), "{m}"), + other => panic!("{other:?}"), + } + } + + #[test] + fn missing_socket_is_unreachable() { + let r = DaemonClient::new("/nonexistent/hackd.sock", Duration::from_millis(100)).status(); + assert!(matches!(r, Err(DaemonError::Unreachable(_)))); + } + + #[test] + fn socket_path_resolution() { + assert_eq!( + default_socket_path(Some("/tmp/hh"), Some("/u")), + Some(PathBuf::from("/tmp/hh/daemon/hackd.sock")) + ); + // A relative HACK_HOME is ignored, as the CLI requires an absolute one. + assert_eq!( + default_socket_path(Some("rel"), Some("/u")), + Some(PathBuf::from("/u/.hack/daemon/hackd.sock")) + ); + assert_eq!(default_socket_path(None, None), None); + } +} diff --git a/apps/desktop/core/src/fixtures.rs b/apps/desktop/core/src/fixtures.rs new file mode 100644 index 000000000..e32b708c1 --- /dev/null +++ b/apps/desktop/core/src/fixtures.rs @@ -0,0 +1,454 @@ +//! Canned data for every state in `apps/desktop/DESIGN.md`, so each view can +//! be shown and tested without a daemon, Docker or a Hack home. +//! +//! Project and host names are placeholders. + +use std::collections::BTreeMap; + +use crate::models::{ + Autostart, AutostartFailures, Container, DaemonApiStatus, DoctorCheck, DoctorReport, + InputDiagnostic, LogEntry, ProjectRow, ProjectRuntime, ProjectsPayload, RuntimeService, +}; +use crate::source::{Snapshot, SourceError}; +use crate::state::{Action, LocalContext}; + +/// Every designed state that can be shown with `--fixtures `. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Fixture { + // List states. + Loading, + Empty, + DaemonDown, + DaemonMismatch, + EngineDown, + Error, + CliMissing, + /// More projects than fit in the sidebar, and more services than fit + /// in the detail pane (both scroll). + ManyProjects, + // Detail states (selected project). + Stopped, + Starting, + Running, + Degraded, + Stopping, + FailedStart, + NeedsAttention, +} + +impl Fixture { + pub const ALL: [Self; 15] = [ + Self::Loading, + Self::Empty, + Self::DaemonDown, + Self::DaemonMismatch, + Self::EngineDown, + Self::Error, + Self::CliMissing, + Self::ManyProjects, + Self::Stopped, + Self::Starting, + Self::Running, + Self::Degraded, + Self::Stopping, + Self::FailedStart, + Self::NeedsAttention, + ]; + + #[must_use] + pub fn name(self) -> &'static str { + match self { + Self::Loading => "loading", + Self::Empty => "empty", + Self::DaemonDown => "daemon-down", + Self::DaemonMismatch => "daemon-mismatch", + Self::EngineDown => "engine-down", + Self::Error => "error", + Self::CliMissing => "cli-missing", + Self::ManyProjects => "many-projects", + Self::Stopped => "stopped", + Self::Starting => "starting", + Self::Running => "running", + Self::Degraded => "degraded", + Self::Stopping => "stopping", + Self::FailedStart => "failed-start", + Self::NeedsAttention => "needs-attention", + } + } + + #[must_use] + pub fn parse(name: &str) -> Option { + Self::ALL.into_iter().find(|f| f.name() == name) + } +} + +pub const CLI_VERSION: &str = "5.0.0"; + +fn container(state: &str, status: &str) -> Container { + Container { + id: None, + name: None, + state: state.into(), + status: status.into(), + } +} + +fn svc(name: &str, c: Container) -> RuntimeService { + RuntimeService { + service: name.into(), + containers: vec![c], + } +} + +fn storefront(services: Vec, status: &str) -> ProjectRow { + let mut hosts = BTreeMap::new(); + hosts.insert("api".into(), vec!["api.storefront.hack".into()]); + hosts.insert("web".into(), vec!["storefront.hack".into()]); + ProjectRow { + project_id: Some("p-storefront".into()), + name: "storefront".into(), + dev_host: Some("storefront.hack".into()), + repo_root: Some("/work/storefront".into()), + status: status.into(), + runtime_status: Some(status.into()), + kind: Some("registered".into()), + defined_services: vec!["api".into(), "db".into(), "web".into(), "worker".into()], + service_hosts: hosts, + runtime: Some(ProjectRuntime { + project: Some("storefront".into()), + services, + }), + backend: Some("compose".into()), + ..ProjectRow::default() + } +} + +fn all_up() -> Vec { + vec![ + svc("api", container("running", "Up 2 hours (healthy)")), + svc("db", container("running", "Up 2 hours")), + svc("web", container("running", "Up 2 hours")), + svc("worker", container("running", "Up 2 hours")), + ] +} + +fn simple(name: &str, status: &str, backend: &str, always_on: bool) -> ProjectRow { + let up = status == "running"; + ProjectRow { + project_id: Some(format!("p-{name}")), + name: name.into(), + dev_host: Some(format!("{name}.hack")), + repo_root: Some(format!("/work/{name}")), + status: if backend == "native" { + "unavailable".into() + } else { + status.into() + }, + // Native rows carry no Compose data; services come from native ps. + defined_services: if backend == "native" { + vec![] + } else { + vec!["app".into()] + }, + runtime: (backend != "native").then(|| ProjectRuntime { + project: Some(name.into()), + services: if up { + vec![svc("app", container("running", "Up 3 days"))] + } else { + vec![] + }, + }), + backend: (backend != "native").then(|| backend.into()), + // Legacy project views refuse native projects with this diagnostic; + // the source then reads native `hack ps`. + input_diagnostic: (backend == "native").then(|| InputDiagnostic { + code: crate::models::NATIVE_PROJECT_CODE.into(), + message: "Native hack.project.json marks this project.".into(), + }), + native: (backend == "native").then(|| crate::models::NativePs { + status: if up { "observed" } else { "not_started" }.into(), + items: if up { + vec![crate::models::NativePsItem { + service: "app".into(), + state: "running".into(), + ..Default::default() + }] + } else { + vec![] + }, + }), + autostart: always_on.then(|| Autostart { + enabled: true, + state: "on".into(), + ..Autostart::default() + }), + ..ProjectRow::default() + } +} + +fn others() -> Vec { + vec![ + simple("docs-site", "running", "native", true), + simple("billing-api", "stopped", "compose", false), + simple("notes", "stopped", "compose", false), + ] +} + +/// The projects payload for a fixture. +#[must_use] +pub fn project(f: Fixture) -> ProjectsPayload { + let main = match f { + Fixture::Stopped | Fixture::Starting => storefront(vec![], "stopped"), + Fixture::Degraded => { + let mut s = all_up(); + s[3] = svc("worker", container("exited", "Exited (1) 4 minutes ago")); + storefront(s, "running") + } + Fixture::FailedStart => storefront( + vec![ + svc("api", container("exited", "Exited (1) 20 seconds ago")), + svc("db", container("exited", "Exited (0) 20 seconds ago")), + svc("worker", container("exited", "Exited (2) 20 seconds ago")), + ], + "stopped", + ), + Fixture::NeedsAttention => { + let mut r = storefront(vec![], "stopped"); + r.autostart = Some(Autostart { + enabled: true, + state: "needs_attention".into(), + paused_reason: None, + failures: AutostartFailures { + count: 3, + last_at: Some("2026-01-01T09:00:00Z".into()), + failed_services: vec!["api".into()], + last_error_code: Some("E_STARTUP_INCOMPLETE".into()), + }, + }); + r + } + Fixture::EngineDown => { + let mut r = storefront(vec![], "unknown"); + r.runtime = None; + r + } + _ => storefront(all_up(), "running"), + }; + let mut projects = vec![main]; + projects.extend(others()); + if f == Fixture::NeedsAttention { + // Unrelated: a second way to need attention. + projects[3].input_diagnostic = Some(InputDiagnostic { + code: "E_CONFIG_INVALID".into(), + message: "hack.config.json is not valid JSON".into(), + }); + } + if f == Fixture::EngineDown { + for p in &mut projects { + p.status = "unknown".into(); + p.runtime = None; + if p.native.is_some() { + p.native = Some(crate::models::NativePs { + status: "runtime_not_running".into(), + items: vec![], + }); + } + } + } + ProjectsPayload { + generated_at: Some("2026-01-01T09:00:00Z".into()), + detail_level: None, + runtime_ok: Some(f != Fixture::EngineDown), + runtime_error: (f == Fixture::EngineDown) + .then(|| "Cannot connect to the Docker daemon".into()), + projects, + } +} + +/// The project a fixture selects in the detail pane. +#[must_use] +pub fn selected(_f: Fixture) -> &'static str { + "storefront" +} + +/// The app-side context (in-flight action, last failure) for a fixture. +#[must_use] +pub fn local(f: Fixture) -> LocalContext { + match f { + Fixture::Starting => LocalContext { + in_flight: Some(Action::Start), + last_failed: vec![], + }, + Fixture::Stopping => LocalContext { + in_flight: Some(Action::Stop), + last_failed: vec![], + }, + _ => LocalContext::default(), + } +} + +fn doctor(f: Fixture) -> DoctorReport { + let mut checks = vec![ + DoctorCheck { + id: "docker daemon".into(), + status: if f == Fixture::EngineDown { + "error" + } else { + "ok" + } + .into(), + detail: String::new(), + }, + DoctorCheck { + id: "caddy local ca".into(), + status: "warn".into(), + detail: "Local CA is not trusted by the system".into(), + }, + ]; + if f == Fixture::NeedsAttention { + checks.push(DoctorCheck { + id: "project config".into(), + status: "error".into(), + detail: "notes: hack.config.json is not valid JSON".into(), + }); + } + DoctorReport { + checks, + summary: vec![], + } +} + +/// A complete snapshot for a fixture, as the live source would produce it. +/// +/// # Errors +/// Returns the error a live source would report for `Error` and `CliMissing`. +pub fn snapshot(f: Fixture) -> Result { + match f { + Fixture::Loading => return Err(SourceError::Pending), + Fixture::CliMissing => return Err(SourceError::CliMissing), + Fixture::Error => { + return Err(SourceError::Cli { + code: "E_UNEXPECTED".into(), + message: "projects registry could not be read".into(), + failed: vec![], + }); + } + _ => {} + } + let mut projects = project(f); + if f == Fixture::Empty { + projects.projects.clear(); + } + if f == Fixture::ManyProjects { + // The selected project also has more services than fit (scrolling). + if let Some(main) = projects.projects.first_mut() { + for i in 1..=24 { + let name = format!("job-{i:02}"); + main.defined_services.push(name.clone()); + if let Some(rt) = main.runtime.as_mut() { + rt.services + .push(svc(&name, container("running", "Up 2 hours"))); + } + } + } + for i in 1..=40 { + let status = if i % 3 == 0 { "running" } else { "stopped" }; + projects + .projects + .push(simple(&format!("project-{i:02}"), status, "compose", false)); + } + } + let daemon = match f { + Fixture::DaemonDown => None, + Fixture::DaemonMismatch => Some(DaemonApiStatus { + status: "ok".into(), + version: "4.3.0".into(), + ..DaemonApiStatus::default() + }), + _ => Some(DaemonApiStatus { + status: "ok".into(), + version: CLI_VERSION.into(), + ..DaemonApiStatus::default() + }), + }; + Ok(Snapshot { + cli_version: CLI_VERSION.into(), + projects, + daemon, + doctor: Some(doctor(f)), + }) +} + +/// A short log tail for the selected project. +#[must_use] +pub fn log_tail(f: Fixture) -> Vec { + let line = |service: &str, level: &str, message: &str| LogEntry { + service: Some(service.into()), + message: message.into(), + stream: Some("stdout".into()), + timestamp: Some("09:00:01".into()), + level: Some(level.into()), + truncated: None, + }; + match f { + Fixture::Stopped + | Fixture::Loading + | Fixture::Empty + | Fixture::CliMissing + | Fixture::EngineDown => vec![], + Fixture::FailedStart => vec![ + line( + "db", + "info", + "database system is ready to accept connections", + ), + line("api", "error", "Error: connect ECONNREFUSED 127.0.0.1:6379"), + line("worker", "error", "fatal: missing required env QUEUE_URL"), + ], + Fixture::Degraded => vec![ + line("api", "info", "GET /health 200 2ms"), + line("worker", "error", "panic: queue connection lost"), + line("web", "info", "ready on :3000"), + ], + _ => vec![ + line("db", "info", "checkpoint complete"), + line("api", "info", "GET /health 200 2ms"), + line("web", "info", "ready on :3000"), + line("worker", "info", "processed 12 jobs"), + ], + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn names_round_trip() { + for f in Fixture::ALL { + assert_eq!(Fixture::parse(f.name()), Some(f)); + } + assert_eq!(Fixture::parse("nope"), None); + } + + #[test] + fn every_fixture_has_a_snapshot_or_designed_error() { + for f in Fixture::ALL { + match (f, snapshot(f)) { + (Fixture::Loading, Err(SourceError::Pending)) + | (Fixture::CliMissing, Err(SourceError::CliMissing)) + | (Fixture::Error, Err(SourceError::Cli { .. })) => {} + (_, Ok(s)) => assert_eq!(s.projects.projects.is_empty(), f == Fixture::Empty), + (f, other) => panic!("{f:?}: {other:?}"), + } + } + } + + #[test] + fn fixtures_hold_no_private_paths() { + for f in Fixture::ALL { + let json = serde_json::to_string(&project(f)).unwrap(); + assert!(!json.contains("/home/") && !json.contains("/Users/")); + } + } +} diff --git a/apps/desktop/core/src/lib.rs b/apps/desktop/core/src/lib.rs new file mode 100644 index 000000000..346879338 --- /dev/null +++ b/apps/desktop/core/src/lib.rs @@ -0,0 +1,19 @@ +//! Hack desktop app core: no GUI dependency. +//! +//! - [`models`]: typed views of `hack ... --json` and `hackd` payloads. +//! - [`runner`]: runs the CLI as the user with timeouts. +//! - [`daemon`]: read-only `hackd` socket client. +//! - [`state`]: project display state and actions. +//! - [`viewmodel`]: list and detail state machines. +//! - [`source`]: live and fixture data sources; [`fixtures`]: canned states. +//! +//! The app keeps no state of its own beyond window layout and holds no +//! credentials; every action is one CLI command. + +pub mod daemon; +pub mod fixtures; +pub mod models; +pub mod runner; +pub mod source; +pub mod state; +pub mod viewmodel; diff --git a/apps/desktop/core/src/models.rs b/apps/desktop/core/src/models.rs new file mode 100644 index 000000000..d8aafb9d2 --- /dev/null +++ b/apps/desktop/core/src/models.rs @@ -0,0 +1,442 @@ +//! Typed views of what `hack ... --json` and `hackd` return. +//! +//! Every struct is lenient (`#[serde(default)]`): the CLI adds fields over +//! time and the app must keep working on older and newer payloads. Unknown +//! fields are ignored; missing ones take their default. Values that the app +//! cannot interpret stay as strings, so "unknown" is never turned into +//! "stopped". + +use serde::{Deserialize, Deserializer, Serialize}; + +/// Treat an explicit JSON `null` like a missing field. The CLI emits `null` +/// for collections of projects it could not read (missing Compose file, +/// input diagnostic); one such row must not fail the whole payload. +fn null_default<'de, D, T>(d: D) -> Result +where + D: Deserializer<'de>, + T: Default + Deserialize<'de>, +{ + Ok(Option::::deserialize(d)?.unwrap_or_default()) +} + +/// `GET /v1/projects` and `hack projects --json` (same shape). +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct ProjectsPayload { + pub generated_at: Option, + pub detail_level: Option, + /// `false` when the container runtime could not be read. + pub runtime_ok: Option, + pub runtime_error: Option, + #[serde(deserialize_with = "null_default")] + pub projects: Vec, +} + +impl ProjectsPayload { + /// Parse a payload that must carry a `projects` array. `{}` or a + /// payload without it is malformed, not an empty registry. + /// + /// # Errors + /// A message naming what is wrong. + pub fn from_value(v: serde_json::Value) -> Result { + if !v.get("projects").is_some_and(serde_json::Value::is_array) { + return Err("projects payload has no projects array".into()); + } + serde_json::from_value(v).map_err(|e| e.to_string()) + } + + /// The engine is known to be unavailable (not merely unreported). + #[must_use] + pub fn engine_down(&self) -> bool { + self.runtime_ok == Some(false) + } +} + +/// One project row. Holds the union of the summary and full row fields. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct ProjectRow { + pub project_id: Option, + #[serde(deserialize_with = "null_default")] + pub name: String, + pub dev_host: Option, + pub repo_root: Option, + /// `running | stopped | missing | unregistered | unavailable | unknown`. + #[serde(deserialize_with = "null_default")] + pub status: String, + /// Adds `not_configured` to the `status` values. + pub runtime_status: Option, + pub kind: Option, + #[serde(deserialize_with = "null_default")] + pub defined_services: Vec, + #[serde(deserialize_with = "null_default")] + pub service_hosts: std::collections::BTreeMap>, + pub input_diagnostic: Option, + pub runtime: Option, + // Summary-only counters. + pub defined_service_count: Option, + pub container_count: Option, + pub running_container_count: Option, + /// Not in the CLI yet (design request R12): always-on state. + pub autostart: Option, + /// Not in the CLI yet (design request R2): `compose` or `native`. + pub backend: Option, + /// Native observation from `hack ps --json` (filled by the source). + #[serde(skip)] + pub native: Option, +} + +/// Project views refuse canonical native projects with this code. +pub const NATIVE_PROJECT_CODE: &str = "E_NATIVE_PROJECT_UNSUPPORTED"; + +impl ProjectRow { + /// A native-backend project: reported by `backend`, or recognised by the + /// input diagnostic legacy project views return for it. + #[must_use] + pub fn is_native(&self) -> bool { + self.backend.as_deref() == Some("native") + || self + .input_diagnostic + .as_ref() + .is_some_and(|d| d.code == NATIVE_PROJECT_CODE) + } +} + +/// `HACK_RUNTIME_BACKEND=native hack ps --json` → `data`. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct NativePs { + /// `not_started | pending | runtime_not_running | runtime_busy | + /// provider_pin_changed | observed | owner_unconfirmed | runtime_degraded`. + #[serde(deserialize_with = "null_default")] + pub status: String, + #[serde(deserialize_with = "null_default")] + pub items: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct NativePsItem { + #[serde(deserialize_with = "null_default")] + pub service: String, + #[serde(deserialize_with = "null_default")] + pub state: String, + pub health: Option, + #[serde(rename = "exitCode")] + pub exit_code: Option, + /// `started | healthy | completed` when the native runtime reports it. + pub readiness: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct InputDiagnostic { + pub code: String, + pub message: String, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct ProjectRuntime { + pub project: Option, + #[serde(deserialize_with = "null_default")] + pub services: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct RuntimeService { + #[serde(deserialize_with = "null_default")] + pub service: String, + #[serde(deserialize_with = "null_default")] + pub containers: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct Container { + pub id: Option, + pub name: Option, + /// Docker state: `running`, `exited`, `created`, `restarting`, ... + #[serde(deserialize_with = "null_default")] + pub state: String, + /// Docker status text, for example `Up 5 minutes (healthy)`. + #[serde(deserialize_with = "null_default")] + pub status: String, +} + +impl Container { + #[must_use] + pub fn is_running(&self) -> bool { + self.state.eq_ignore_ascii_case("running") + } + + #[must_use] + pub fn is_unhealthy(&self) -> bool { + self.status.contains("(unhealthy)") + } + + /// Exit code from Docker's exited or restarting status text. + #[must_use] + pub fn exit_code(&self) -> Option { + let rest = self + .status + .strip_prefix("Exited (") + .or_else(|| self.status.strip_prefix("Restarting ("))?; + rest.split(')').next()?.trim().parse().ok() + } + + /// Uptime text from a status like `Up 2 hours (healthy)`. + #[must_use] + pub fn uptime(&self) -> Option<&str> { + let rest = self.status.strip_prefix("Up ")?; + Some(rest.split(" (").next().unwrap_or(rest).trim()) + } +} + +/// Always-on state as designed for HACK-2213 (not yet emitted by the CLI). +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct Autostart { + pub enabled: bool, + /// `off | on | paused | starting | needs_attention`. + #[serde(deserialize_with = "null_default")] + pub state: String, + pub paused_reason: Option, + pub failures: AutostartFailures, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct AutostartFailures { + pub count: u32, + pub last_at: Option, + pub failed_services: Vec, + pub last_error_code: Option, +} + +/// `GET /v1/status` on the hackd socket. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct DaemonApiStatus { + #[serde(deserialize_with = "null_default")] + pub status: String, + pub version: String, + pub pid: Option, + pub started_at: Option, + pub uptime_ms: Option, +} + +/// `hack daemon status --json` (raw object, no envelope). +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct DaemonStatus { + /// `running | starting | incompatible | stale | stopped`. + #[serde(deserialize_with = "null_default")] + pub status: String, + pub running: bool, + pub api_ok: bool, + pub api_compatible: Option, + pub stale: bool, + pub stale_reason: Option, + pub issue: Option, + pub next_step: Option, + pub pid: Option, + pub service: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct DaemonService { + /// `launchd | systemd`. + pub manager: Option, + pub available: bool, + pub installed: bool, + pub enabled: bool, + pub active: bool, + pub summary: Option, + pub next_step: Option, +} + +/// `hack doctor --json` → `data`. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct DoctorReport { + #[serde(deserialize_with = "null_default")] + pub checks: Vec, + #[serde(deserialize_with = "null_default")] + pub summary: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct DoctorCheck { + pub id: String, + /// `ok | warn | error`. + #[serde(deserialize_with = "null_default")] + pub status: String, + pub detail: String, +} + +impl DoctorReport { + /// Findings that need the user (warn + error), shown as the sidebar badge. + #[must_use] + pub fn attention_count(&self) -> usize { + self.checks.iter().filter(|c| c.status != "ok").count() + } +} + +/// `hack up|down|restart --json` → `data`. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct LifecycleResult { + pub action: String, + pub project: String, + pub services: LifecycleServices, + #[serde(rename = "durationMs")] + pub duration_ms: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct LifecycleServices { + pub started: Vec, + pub completed: Vec, + pub stopped: Vec, + pub failed: Vec, +} + +/// `hack open --json`. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct OpenResult { + pub url: String, + pub hint: Option, +} + +/// One line of `hack logs --json` (ndjson). +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct LogEvent { + /// `start | log | notice | heartbeat | error | end`. + #[serde(rename = "type")] + pub kind: String, + pub ts: Option, + pub backend: Option, + pub entry: Option, + pub message: Option, + pub reason: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct LogEntry { + pub service: Option, + pub message: String, + pub stream: Option, + pub timestamp: Option, + pub level: Option, + pub truncated: Option, +} + +/// The `{ok:false,error}` half of the CLI result envelope. +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +#[serde(default)] +pub struct CliError { + pub code: String, + pub message: String, + /// Structured detail, for example `E_STARTUP_INCOMPLETE`'s + /// `{running, completed, failed}` service lists. + pub detail: Option, +} + +impl CliError { + /// Services named in `detail.failed` (startup failures). + #[must_use] + pub fn failed_services(&self) -> Vec { + self.detail + .as_ref() + .and_then(|d| d.get("failed")) + .and_then(|f| f.as_array()) + .map(|a| { + a.iter() + .filter_map(|v| v.as_str().map(str::to_owned)) + .collect() + }) + .unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn container_status_parsing() { + let up = Container { + state: "running".into(), + status: "Up 2 hours (healthy)".into(), + ..Container::default() + }; + assert!(up.is_running()); + assert_eq!(up.uptime(), Some("2 hours")); + assert_eq!(up.exit_code(), None); + + let exited = Container { + state: "exited".into(), + status: "Exited (137) 3 minutes ago".into(), + ..Container::default() + }; + assert!(!exited.is_running()); + assert_eq!(exited.exit_code(), Some(137)); + assert_eq!(exited.uptime(), None); + } + + #[test] + fn projects_payload_is_lenient() { + let raw = r#"{"generated_at":"t","runtime_ok":true,"unknown_field":1, + "projects":[{"name":"alpha","status":"running","extra":{"x":1}}]}"#; + let p: ProjectsPayload = serde_json::from_str(raw).unwrap(); + assert_eq!(p.projects[0].name, "alpha"); + assert!(p.projects[0].defined_services.is_empty()); + assert!(!p.engine_down()); + } + + #[test] + fn explicit_nulls_do_not_fail_the_payload() { + // Shape of an unreadable row from serializeProjectView(). + let raw = r#"{"projects":[{"name":"broken","status":"unavailable", + "defined_services":null,"service_hosts":null,"runtime":null, + "input_diagnostic":{"code":"E_CONFIG_INVALID","message":"bad"}}, + {"name":"ok","status":"running","defined_services":["api"]}]}"#; + let p: ProjectsPayload = serde_json::from_str(raw).unwrap(); + assert_eq!(p.projects.len(), 2); + assert!(p.projects[0].defined_services.is_empty()); + assert!(p.projects[0].service_hosts.is_empty()); + } + + #[test] + fn payload_without_projects_is_malformed() { + assert!(ProjectsPayload::from_value(serde_json::json!({})).is_err()); + assert!(ProjectsPayload::from_value(serde_json::json!({"projects": null})).is_err()); + assert!(ProjectsPayload::from_value(serde_json::json!({"projects": []})).is_ok()); + } + + #[test] + fn startup_failure_detail_is_kept() { + let e: CliError = serde_json::from_str( + r#"{"code":"E_STARTUP_INCOMPLETE","message":"m","detail":{"running":["db"],"completed":[],"failed":["api","worker"]}}"#, + ) + .unwrap(); + assert_eq!(e.failed_services(), vec!["api", "worker"]); + } + + #[test] + fn doctor_attention_count() { + let r: DoctorReport = serde_json::from_str( + r#"{"checks":[{"id":"a","status":"ok"},{"id":"b","status":"warn"},{"id":"c","status":"error"}]}"#, + ) + .unwrap(); + assert_eq!(r.attention_count(), 2); + } +} diff --git a/apps/desktop/core/src/runner.rs b/apps/desktop/core/src/runner.rs new file mode 100644 index 000000000..e41886c2a --- /dev/null +++ b/apps/desktop/core/src/runner.rs @@ -0,0 +1,911 @@ +//! Runs `hack ... --json` as the user, with a timeout. +//! +//! Contract: +//! - Each command runs in its own process group. On timeout the whole group +//! is killed (SIGKILL) and reaped, so no `docker compose` child outlives it. +//! - stdout is parsed as JSON. The CLI result envelope (`{ok,data}` / +//! `{ok:false,error}`) is unwrapped; raw objects are returned as they are. +//! - stderr is kept (last 8 KB) only for error messages; it is never parsed. +//! - `HACK_NO_INTERACTIVE=1` is always set: the app never answers prompts. + +use std::io::{BufRead, BufReader, Read}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +use serde::de::DeserializeOwned; +use serde_json::Value; + +use crate::models::{CliError, LogEvent}; + +const STDERR_KEEP: usize = 8 * 1024; +/// Most stdout kept from one command; more is an error (`OutputTooLarge`), +/// never silently parsed half-way. Far above any real `--json` result. +pub const MAX_STDOUT: usize = 64 * 1024 * 1024; + +#[derive(Debug, Clone, PartialEq)] +pub enum RunError { + /// The `hack` executable was not found. + NotFound, + Spawn(String), + Timeout { + after: Duration, + }, + /// The CLI returned an error envelope. + Cli(CliError), + /// Non-zero exit without an error envelope. + Exit { + code: Option, + stderr: String, + }, + /// stdout was not the JSON we expected. + BadJson { + error: String, + stdout_head: String, + }, +} + +impl std::fmt::Display for RunError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound => write!(f, "hack was not found on PATH"), + Self::Spawn(e) => write!(f, "could not run hack: {e}"), + Self::Timeout { after } => write!(f, "hack did not answer within {after:?}"), + Self::Cli(e) => write!(f, "{}: {}", e.code, e.message), + Self::Exit { code, stderr } => { + write!(f, "hack exited with {code:?}: {}", stderr.trim()) + } + Self::BadJson { error, .. } => write!(f, "unexpected output from hack: {error}"), + } + } +} + +#[derive(Debug, Clone)] +pub struct CliRunner { + program: PathBuf, + env: Vec<(String, String)>, + /// `on_path` found no executable: spawn reports `NotFound`. + missing: bool, + /// How long a timed-out command gets after SIGTERM before SIGKILL. + term_grace: Duration, +} + +/// Default grace after SIGTERM. The CLI forwards SIGTERM to the detached +/// subprocess groups it owns (for example a Compose build) and waits for +/// them; SIGKILL alone would orphan those and kill the CLI's own timer. +pub const TERM_GRACE: Duration = Duration::from_secs(15); + +impl CliRunner { + /// Run `program` (normally `hack`, resolved through PATH). + #[must_use] + pub fn new(program: impl Into) -> Self { + Self { + program: program.into(), + env: vec![("HACK_NO_INTERACTIVE".into(), "1".into())], + missing: false, + term_grace: TERM_GRACE, + } + } + + /// Change the SIGTERM-to-SIGKILL grace for timed-out commands. + #[must_use] + pub fn with_term_grace(mut self, grace: Duration) -> Self { + self.term_grace = grace; + self + } + + /// Find `name` on `PATH` and run it by absolute path. + /// + /// Only absolute `PATH` entries are searched: an empty or relative entry + /// (such as `.`) would resolve against the working directory, which + /// another local user may control. When nothing is found the runner + /// reports `NotFound` without spawning anything. + #[must_use] + pub fn on_path(name: &str) -> Self { + let path = std::env::var_os("PATH"); + match resolve_on_path(name, path.as_deref()) { + Some(p) => Self::new(p), + None => { + let mut r = Self::new(name); + r.missing = true; + r + } + } + } + + /// Add an environment variable for every command (tests use this for + /// a temporary `HACK_HOME`). + #[must_use] + pub fn with_env(mut self, key: &str, value: &str) -> Self { + self.env.push((key.into(), value.into())); + self + } + + fn command(&self, args: &[&str], path: Option<&Path>) -> Command { + use std::os::unix::process::CommandExt; + let mut cmd = Command::new(&self.program); + cmd.args(args); + if let Some(p) = path { + cmd.arg("--path").arg(p); + } + // Children (and their own children, such as `docker` run by + // `hack doctor`) get a PATH with only absolute entries: an empty or + // relative entry would resolve against the working directory. + if let Some(path) = std::env::var_os("PATH") { + cmd.env("PATH", sanitize_path(&path)); + } + for (k, v) in &self.env { + cmd.env(k, v); + } + cmd.stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .process_group(0); + cmd + } + + fn spawn(&self, args: &[&str], path: Option<&Path>) -> Result { + if self.missing { + return Err(RunError::NotFound); + } + // ETXTBSY: the executable was just written (a fresh upgrade, or a test + // script whose write descriptor a concurrent fork still holds). Retry + // briefly instead of reporting a spawn failure. + let mut attempt = 0; + loop { + match self.command(args, path).spawn() { + Ok(c) => return Ok(c), + Err(e) if e.raw_os_error() == Some(libc::ETXTBSY) && attempt < 20 => { + attempt += 1; + thread::sleep(Duration::from_millis(10)); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Err(RunError::NotFound); + } + Err(e) => return Err(RunError::Spawn(e.to_string())), + } + } + } + + /// Run and return raw stdout, stderr tail and exit code. + /// + /// # Errors + /// `NotFound`, `Spawn` or `Timeout`. + pub fn run_raw( + &self, + args: &[&str], + path: Option<&Path>, + timeout: Duration, + ) -> Result<(String, String, Option), RunError> { + self.run_inner(args, path, Some(timeout)) + } + + /// Run with no deadline from the app: wait until the command exits and + /// its output pipes close. For lifecycle commands, whose timeouts the + /// CLI owns; terminating them from outside can orphan the detached + /// build subprocesses the CLI starts. + /// + /// # Errors + /// Any [`RunError`] except `Timeout`. + pub fn run_json_to_completion( + &self, + args: &[&str], + path: Option<&Path>, + ) -> Result { + let (stdout, stderr, code) = self.run_inner(args, path, None)?; + let value = parse_result(&stdout, &stderr, code)?; + serde_json::from_value(value).map_err(|e| RunError::BadJson { + error: e.to_string(), + stdout_head: head(&stdout), + }) + } + + fn run_inner( + &self, + args: &[&str], + path: Option<&Path>, + timeout: Option, + ) -> Result<(String, String, Option), RunError> { + let mut child = self.spawn(args, path)?; + let out = read_all(child.stdout.take(), Keep::Head(MAX_STDOUT)); + let err = read_all(child.stderr.take(), Keep::Tail(STDERR_KEEP)); + let deadline = timeout.map(|t| (Instant::now() + t, t)); + let status = loop { + match child.try_wait() { + Ok(Some(s)) => break s, + Ok(None) if deadline.is_some_and(|(d, _)| Instant::now() >= d) => { + terminate_group(&mut child, self.term_grace); + let after = deadline.map_or(Duration::ZERO, |(_, t)| t); + return Err(RunError::Timeout { after }); + } + Ok(None) => thread::sleep(Duration::from_millis(10)), + Err(e) => return Err(RunError::Spawn(e.to_string())), + } + }; + // A deadline also covers draining: a descendant that keeps a pipe + // open after `hack` exits must not hold the caller past the timeout. + // Without a deadline, wait for the pipes to close. + let grace = self.term_grace; + let drain = |rx: &mpsc::Receiver, child: &mut Child| match deadline { + Some((d, t)) => { + let left = d.saturating_duration_since(Instant::now()); + rx.recv_timeout(left).map_err(|_| { + terminate_group(child, grace); + RunError::Timeout { after: t } + }) + } + None => rx + .recv() + .map_err(|_| RunError::Spawn("output reader stopped".into())), + }; + let stdout = drain(&out, &mut child)?; + let stderr = drain(&err, &mut child)?; + if let Some(e) = stdout.error.as_ref().or(stderr.error.as_ref()) { + return Err(RunError::Spawn(format!( + "reading the command's output failed: {e}" + ))); + } + if stdout.overflow { + return Err(RunError::BadJson { + error: format!("output larger than {MAX_STDOUT} bytes"), + stdout_head: head(&stdout.text), + }); + } + Ok((stdout.text, stderr.text, status.code())) + } + + /// Run a `--json` command and deserialize its (unwrapped) result. + /// + /// # Errors + /// Any [`RunError`]. + pub fn run_json( + &self, + args: &[&str], + path: Option<&Path>, + timeout: Duration, + ) -> Result { + let (stdout, stderr, code) = self.run_raw(args, path, timeout)?; + let value = parse_result(&stdout, &stderr, code)?; + serde_json::from_value(value).map_err(|e| RunError::BadJson { + error: e.to_string(), + stdout_head: head(&stdout), + }) + } + + /// `hack --version` → `"5.0.0"` (text output until request R1). + /// + /// # Errors + /// Any [`RunError`]. + pub fn version(&self, timeout: Duration) -> Result { + let (stdout, stderr, code) = self.run_raw(&["--version"], None, timeout)?; + if code != Some(0) { + return Err(RunError::Exit { code, stderr }); + } + parse_version(&stdout).ok_or_else(|| RunError::BadJson { + error: "no version in output".into(), + stdout_head: head(&stdout), + }) + } + + /// Start a long-running ndjson stream (`hack logs --json ...`). + /// + /// # Errors + /// `NotFound` or `Spawn`. + pub fn stream_logs(&self, args: &[&str], path: Option<&Path>) -> Result { + let mut child = self.spawn(args, path)?; + let stdout = child.stdout.take(); + let (tx, rx) = mpsc::channel(); + let reader = thread::spawn(move || { + let Some(stdout) = stdout else { return }; + for line in BufReader::new(stdout).lines() { + let Ok(line) = line else { break }; + if line.trim().is_empty() { + continue; + } + let event = serde_json::from_str::(&line).unwrap_or_else(|_| LogEvent { + kind: "notice".into(), + message: Some(line), + ..LogEvent::default() + }); + if tx.send(event).is_err() { + break; + } + } + }); + Ok(LogStream { + child, + rx, + reader: Some(reader), + }) + } +} + +/// A running log stream. Dropping it kills the process group. +pub struct LogStream { + child: Child, + rx: mpsc::Receiver, + reader: Option>, +} + +impl LogStream { + /// Events received so far, without blocking. + pub fn drain(&self) -> Vec { + self.rx.try_iter().collect() + } + + /// Wait up to `timeout` for the next event. + pub fn next_timeout(&self, timeout: Duration) -> Option { + self.rx.recv_timeout(timeout).ok() + } +} + +impl Drop for LogStream { + fn drop(&mut self) { + kill_group(&mut self.child); + if let Some(r) = self.reader.take() { + let _ = r.join(); + } + } +} + +/// Which part of a pipe to keep while draining it. +#[derive(Clone, Copy)] +enum Keep { + /// The first `n` bytes; anything beyond marks the output as too large. + Head(usize), + /// Only the last `n` bytes (a rolling window). + Tail(usize), +} + +/// What a drained pipe yielded. +struct Drained { + text: String, + overflow: bool, + /// A read failed before end of file: the output is incomplete. + error: Option, +} + +/// Read a pipe to EOF on a thread, keeping memory bounded the whole time +/// (a lifecycle command can run for hours and print without limit). +fn read_all(r: Option, keep: Keep) -> mpsc::Receiver { + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let mut buf: Vec = Vec::new(); + let mut overflow = false; + let mut error = None; + if let Some(mut r) = r { + let mut chunk = [0u8; 16 * 1024]; + loop { + let n = match r.read(&mut chunk) { + Ok(0) => break, + Ok(n) => n, + // A signal interrupted the read; nothing was lost. + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => { + error = Some(e.to_string()); + break; + } + }; + match keep { + Keep::Head(max) => { + let room = max.saturating_sub(buf.len()); + buf.extend_from_slice(&chunk[..n.min(room)]); + overflow |= n > room; + } + Keep::Tail(max) => { + buf.extend_from_slice(&chunk[..n]); + if buf.len() > max { + let cut = buf.len() - max; + buf.drain(..cut); + } + } + } + } + } + let _ = tx.send(Drained { + text: String::from_utf8_lossy(&buf).into_owned(), + overflow, + error, + }); + }); + rx +} + +/// Used when no absolute `PATH` entry remains: an empty `PATH` would make +/// a shell search its working directory. +pub const FALLBACK_PATH: &str = "/usr/local/bin:/usr/bin:/bin"; + +/// `PATH` with empty and relative entries removed; never empty. +#[must_use] +pub fn sanitize_path(path: &std::ffi::OsStr) -> std::ffi::OsString { + let kept = std::env::join_paths(std::env::split_paths(path).filter(|d| d.is_absolute())) + .unwrap_or_default(); + if kept.is_empty() { + std::ffi::OsString::from(FALLBACK_PATH) + } else { + kept + } +} + +/// Search absolute `PATH` entries for an executable regular file `name`. +#[must_use] +pub fn resolve_on_path(name: &str, path: Option<&std::ffi::OsStr>) -> Option { + use std::os::unix::fs::PermissionsExt; + if name.contains('/') { + let p = PathBuf::from(name); + return p.is_absolute().then_some(p); + } + std::env::split_paths(path?) + .filter(|dir| dir.is_absolute()) + .map(|dir| dir.join(name)) + .find(|p| { + std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0) + }) +} + +/// Ask the process group to stop (SIGTERM), give the CLI `grace` to retire +/// the subprocesses it owns, then SIGKILL whatever is left of the group and +/// reap. Returns only once the direct child has exited. +fn terminate_group(child: &mut Child, grace: Duration) { + if let Ok(pid) = i32::try_from(child.id()) { + // SAFETY: plain syscall on the group we created with process_group(0). + unsafe { + libc::kill(-pid, libc::SIGTERM); + } + } + let until = Instant::now() + grace; + while Instant::now() < until { + if matches!(child.try_wait(), Ok(Some(_))) { + break; + } + thread::sleep(Duration::from_millis(20)); + } + kill_group(child); +} + +fn kill_group(child: &mut Child) { + if let Ok(pid) = i32::try_from(child.id()) { + // SAFETY: plain syscall on the group we created with process_group(0). + unsafe { + libc::kill(-pid, libc::SIGKILL); + } + } + let _ = child.kill(); + let _ = child.wait(); +} + +fn head(s: &str) -> String { + s.chars().take(200).collect() +} + +/// Unwrap the CLI result envelope, or return a raw object as is. +/// +/// # Errors +/// `Cli` for an error envelope, `Exit` for a non-zero exit with no JSON, +/// `BadJson` for unparsable stdout. +pub fn parse_result(stdout: &str, stderr: &str, code: Option) -> Result { + let trimmed = stdout.trim(); + if trimmed.is_empty() { + return Err(RunError::Exit { + code, + stderr: stderr.to_owned(), + }); + } + let value: Value = match serde_json::from_str(trimmed) { + Ok(v) => v, + Err(e) if code == Some(0) => { + return Err(RunError::BadJson { + error: e.to_string(), + stdout_head: head(stdout), + }); + } + Err(_) => { + return Err(RunError::Exit { + code, + stderr: stderr.to_owned(), + }); + } + }; + match value.get("ok").and_then(Value::as_bool) { + Some(false) => { + let err = value + .get("error") + .cloned() + .and_then(|e| match e { + Value::String(message) => Some(CliError { + code: "E_UNEXPECTED".into(), + message, + detail: None, + }), + other => serde_json::from_value::(other).ok(), + }) + .unwrap_or_else(|| CliError { + code: "E_UNEXPECTED".into(), + message: head(stdout), + detail: None, + }); + Err(RunError::Cli(err)) + } + Some(true) if value.get("data").is_some() => Ok(value["data"].clone()), + // Raw objects. Some commands (doctor, env list) exit 1 on findings + // but still print a full report, which the app wants. + _ => Ok(value), + } +} + +/// `hack v5.0.0` / `5.0.0-next.3` → the version string. +#[must_use] +pub fn parse_version(text: &str) -> Option { + text.split_whitespace() + .map(|w| w.trim_start_matches('v')) + .find(|w| w.chars().next().is_some_and(|c| c.is_ascii_digit()) && w.contains('.')) + .map(str::to_owned) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::os::unix::fs::PermissionsExt; + + /// A fake `hack` in a fresh temp dir: a shell script with a fixed body. + struct FakeHack { + dir: PathBuf, + } + + impl FakeHack { + fn new(body: &str) -> Self { + let dir = std::env::temp_dir().join(format!( + "hack-desktop-runner-{}-{}", + std::process::id(), + rand_suffix() + )); + std::fs::create_dir_all(&dir).unwrap(); + let p = dir.join("hack"); + std::fs::write( + &p, + format!( + "#!/bin/sh\nif [ \"$1\" = __warmup ]; then echo '{{}}'; exit 0; fi\n{body}\n" + ), + ) + .unwrap(); + std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap(); + let fake = Self { dir }; + // macOS may scan a newly written executable on first launch. Keep + // that delay outside every test's product-timeout measurement. + fake.runner() + .run_json::(&["__warmup"], None, Duration::from_secs(60)) + .expect("fake CLI warmup"); + fake + } + fn runner(&self) -> CliRunner { + CliRunner::new(self.dir.join("hack")).with_env("HACK_HOME", &self.dir.to_string_lossy()) + } + } + + impl Drop for FakeHack { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.dir); + } + } + + fn rand_suffix() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + } + + const T: Duration = Duration::from_secs(5); + + #[test] + fn unwraps_envelope() { + let f = FakeHack::new( + r#"echo '{"ok":true,"data":{"action":"up","project":"p","services":{"failed":["api"]}}}'"#, + ); + let r: crate::models::LifecycleResult = + f.runner().run_json(&["up", "--json"], None, T).unwrap(); + assert_eq!(r.services.failed, vec!["api"]); + } + + #[test] + fn error_envelope_becomes_cli_error() { + let f = FakeHack::new( + r#"echo '{"ok":false,"error":{"code":"E_DOCKER_UNAVAILABLE","message":"down"}}'; exit 1"#, + ); + let r = f.runner().run_json::(&["up"], None, T); + assert_eq!( + r, + Err(RunError::Cli(CliError { + code: "E_DOCKER_UNAVAILABLE".into(), + message: "down".into(), + detail: None, + })) + ); + } + + #[test] + fn raw_object_with_nonzero_exit_is_kept() { + let f = FakeHack::new(r#"echo '{"checks":[{"id":"x","status":"error"}]}'; exit 1"#); + let r: crate::models::DoctorReport = f.runner().run_json(&["doctor"], None, T).unwrap(); + assert_eq!(r.attention_count(), 1); + } + + #[test] + fn passes_path_and_no_interactive() { + let f = FakeHack::new(r#"printf '{"args":"%s","ni":"%s"}' "$*" "$HACK_NO_INTERACTIVE""#); + let v: Value = f + .runner() + .run_json(&["ps", "--json"], Some(Path::new("/work/p")), T) + .unwrap(); + assert_eq!(v["args"], "ps --json --path /work/p"); + assert_eq!(v["ni"], "1"); + } + + /// Evidence that `pid` has exited: `kill(pid, 0)` fails, or (Linux only) + /// `/proc` shows a zombie. An unreadable `/proc` is no evidence. + fn process_gone(pid: i32) -> bool { + // SAFETY: signal 0 only checks existence. + if unsafe { libc::kill(pid, 0) } != 0 { + return true; + } + if cfg!(target_os = "linux") { + return std::fs::read_to_string(format!("/proc/{pid}/stat")) + .is_ok_and(|s| s.contains(") Z ")); + } + false + } + + #[test] + fn timeout_kills_the_process_group() { + // The child spawns a grandchild that would outlive a plain kill. + // FakeHack warms the script before this timed run; `ready` proves + // the body ran, rather than timing out before it exercised cleanup. + let f = FakeHack::new( + "echo ready > \"$HACK_HOME/ready\"\nsleep 30 &\necho $! > \"$HACK_HOME/grandchild\"\nwait", + ); + let started = Instant::now(); + let r = f + .runner() + .with_term_grace(Duration::from_millis(500)) + .run_json::(&["up"], None, Duration::from_secs(2)); + assert!(matches!(r, Err(RunError::Timeout { .. })), "{r:?}"); + assert!(f.dir.join("ready").exists(), "the fake CLI never started"); + assert!(started.elapsed() < Duration::from_secs(5)); + let pid: i32 = std::fs::read_to_string(f.dir.join("grandchild")) + .expect("grandchild pid written") + .trim() + .parse() + .unwrap(); + // Give the kernel a moment to deliver SIGKILL and reparent/reap. + // Positive control: a live process must never count as gone, so + // the assertion below cannot pass just because a probe is missing. + assert!(!process_gone(i32::try_from(std::process::id()).unwrap())); + let gone = (0..100).any(|_| { + thread::sleep(Duration::from_millis(20)); + process_gone(pid) + }); + assert!(gone, "grandchild {pid} survived the timeout"); + } + + #[test] + fn timeout_covers_a_descendant_holding_the_pipe() { + // `hack` exits at once, but a background child keeps stdout open. + let f = FakeHack::new("sleep 30 &\necho '{}'"); + let started = Instant::now(); + let r = f + .runner() + .run_json::(&["x"], None, Duration::from_millis(300)); + assert!(matches!(r, Err(RunError::Timeout { .. })), "{r:?}"); + assert!(started.elapsed() < Duration::from_secs(5)); + } + + #[test] + fn path_lookup_ignores_relative_entries() { + let f = FakeHack::new("echo '{}'"); + let dir = f.dir.to_string_lossy().into_owned(); + let rel = std::ffi::OsString::from(format!(".:{dir}")); + assert_eq!( + resolve_on_path("hack", Some(&rel)), + Some(f.dir.join("hack")) + ); + let only_rel = std::ffi::OsString::from(".:relative/bin:"); + assert_eq!(resolve_on_path("hack", Some(&only_rel)), None); + assert_eq!(resolve_on_path("hack", None), None); + assert_eq!( + resolve_on_path("./hack", Some(&rel)), + None, + "relative names refused" + ); + } + + #[test] + fn child_path_drops_empty_and_relative_entries() { + let p = std::ffi::OsString::from(".::bin:/usr/bin:./x:/bin"); + assert_eq!(sanitize_path(&p), std::ffi::OsString::from("/usr/bin:/bin")); + // All relative or empty: a non-empty trusted fallback, never "". + for bad in [".::relative/bin:", "", ":", "."] { + assert_eq!( + sanitize_path(std::ffi::OsStr::new(bad)), + std::ffi::OsString::from(FALLBACK_PATH), + "{bad:?}" + ); + } + // And the child really receives an all-absolute PATH. + let f = FakeHack::new(r#"printf '{"p":"%s"}' "$PATH""#); + let v: Value = f.runner().run_json(&["x"], None, T).unwrap(); + let got = v["p"].as_str().unwrap(); + assert!( + got.split(':').all(|e| e.starts_with('/')), + "child PATH {got:?}" + ); + } + + #[test] + fn a_runner_that_found_nothing_never_spawns() { + let mut r = CliRunner::new("hack"); + r.missing = true; + assert_eq!( + r.run_json::(&["x"], None, T), + Err(RunError::NotFound) + ); + } + + #[test] + fn timeout_sends_sigterm_first_so_the_cli_can_retire_its_children() { + // `wait` on a background job is interrupted at once by a trapped + // signal in every POSIX shell; a foreground `sleep` loop is not + // portable for this. `ready` separates a slow start from a failure. + let f = FakeHack::new( + r#"trap 'echo term > "$HACK_HOME/got"; exit 0' TERM +echo ready > "$HACK_HOME/ready" +sleep 30 & +wait $!"#, + ); + let started = Instant::now(); + let r = f + .runner() + .with_term_grace(Duration::from_secs(3)) + .run_json::(&["up"], None, Duration::from_secs(2)); + assert!(matches!(r, Err(RunError::Timeout { .. })), "{r:?}"); + assert!(f.dir.join("ready").exists(), "the fake CLI never started"); + assert!(f.dir.join("got").exists(), "SIGTERM reached the CLI"); + assert!( + started.elapsed() < Duration::from_millis(4500), + "exited on TERM without the full grace: {:?}", + started.elapsed() + ); + } + + #[test] + fn a_cli_ignoring_sigterm_is_killed_after_the_grace() { + let f = FakeHack::new("trap '' TERM\nwhile :; do sleep 0.05; done"); + let started = Instant::now(); + let r = f + .runner() + .with_term_grace(Duration::from_millis(300)) + .run_json::(&["up"], None, Duration::from_millis(200)); + assert!(matches!(r, Err(RunError::Timeout { .. })), "{r:?}"); + assert!(started.elapsed() < Duration::from_secs(3)); + } + + #[test] + fn run_to_completion_waits_past_any_short_deadline() { + // Takes longer than a short timeout would allow, and still succeeds. + let f = FakeHack::new("sleep 1; echo '{\"ok\":true,\"data\":{\"action\":\"up\"}}'"); + let started = Instant::now(); + let r: Value = f.runner().run_json_to_completion(&["up"], None).unwrap(); + assert_eq!(r["action"], "up"); + assert!(started.elapsed() >= Duration::from_millis(900)); + } + + /// A reader that replays scripted results. + struct Scripted(std::collections::VecDeque>>); + + impl Read for Scripted { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self.0.pop_front() { + None => Ok(0), + Some(Err(e)) => Err(e), + Some(Ok(bytes)) => { + buf[..bytes.len()].copy_from_slice(&bytes); + Ok(bytes.len()) + } + } + } + } + + #[test] + fn interrupted_reads_are_retried_and_other_errors_reported() { + use std::io::{Error, ErrorKind}; + let r = Scripted( + vec![ + Err(Error::from(ErrorKind::Interrupted)), + Ok(b"{\"ok\":".to_vec()), + Err(Error::from(ErrorKind::Interrupted)), + Ok(b"true}".to_vec()), + ] + .into(), + ); + let d = read_all(Some(r), Keep::Head(1024)).recv().unwrap(); + assert_eq!(d.text, "{\"ok\":true}"); + assert!(d.error.is_none()); + + let r = Scripted( + vec![ + Ok(b"{\"ok\":true".to_vec()), + Err(Error::new(ErrorKind::BrokenPipe, "gone")), + ] + .into(), + ); + let d = read_all(Some(r), Keep::Head(1024)).recv().unwrap(); + assert!(d.error.is_some(), "a failed read is not end of file"); + } + + #[test] + fn pipes_are_drained_within_bounded_memory() { + // 2 MiB to stderr: only the last STDERR_KEEP bytes are retained. + let f = FakeHack::new( + "head -c 2097152 /dev/zero | tr '\\0' e >&2\nprintf 'TAIL' >&2\necho '{}'", + ); + let (out, err, code) = f.runner().run_raw(&["x"], None, T).unwrap(); + assert_eq!(code, Some(0)); + assert_eq!(out.trim(), "{}"); + assert!(err.len() <= STDERR_KEEP, "{}", err.len()); + assert!(err.ends_with("TAIL")); + // Head keeping stops at the cap and flags the overflow. + let rx = read_all(Some(&b"abcdef"[..]), Keep::Head(4)); + let d = rx.recv().unwrap(); + assert_eq!(d.text, "abcd"); + assert!(d.overflow); + } + + #[test] + fn missing_binary_is_not_found() { + let r = CliRunner::new("/nonexistent/hack").run_json::(&["x"], None, T); + assert_eq!(r, Err(RunError::NotFound)); + } + + #[test] + fn bad_json_is_reported() { + let f = FakeHack::new("echo 'not json'"); + assert!(matches!( + f.runner().run_json::(&["x"], None, T), + Err(RunError::BadJson { .. }) + )); + } + + #[test] + fn version_parsing() { + assert_eq!(parse_version("hack v4.3.0\n").as_deref(), Some("4.3.0")); + assert_eq!( + parse_version("5.0.0-next.3").as_deref(), + Some("5.0.0-next.3") + ); + assert_eq!(parse_version("hack"), None); + let f = FakeHack::new("echo 'hack v5.0.0'"); + assert_eq!(f.runner().version(T).unwrap(), "5.0.0"); + } + + #[test] + fn log_stream_parses_ndjson_and_stops_on_drop() { + let f = FakeHack::new( + r#"echo '{"type":"start","backend":"compose"}' +echo '{"type":"log","entry":{"service":"api","message":"hi"}}' +echo 'plain text' +sleep 30"#, + ); + let s = f.runner().stream_logs(&["logs", "--json"], None).unwrap(); + let mut got = Vec::new(); + while got.len() < 3 { + got.push(s.next_timeout(T).expect("event")); + } + assert_eq!(got[0].kind, "start"); + assert_eq!(got[1].entry.as_ref().unwrap().message, "hi"); + assert_eq!(got[2].message.as_deref(), Some("plain text")); + let t = Instant::now(); + drop(s); + assert!(t.elapsed() < Duration::from_secs(5)); + } +} diff --git a/apps/desktop/core/src/source.rs b/apps/desktop/core/src/source.rs new file mode 100644 index 000000000..1c97ea5f0 --- /dev/null +++ b/apps/desktop/core/src/source.rs @@ -0,0 +1,870 @@ +//! Where the app's data comes from: the live CLI and daemon, or fixtures. + +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use crate::daemon::DaemonClient; +use crate::fixtures::{self, Fixture}; +use crate::models::{ + DaemonApiStatus, DoctorReport, LifecycleResult, LogEntry, OpenResult, ProjectsPayload, +}; +use crate::runner::{CliRunner, RunError}; +use crate::state::Action; + +/// Everything the list and detail panes need for one refresh. +#[derive(Debug, Clone, PartialEq)] +pub struct Snapshot { + pub cli_version: String, + pub projects: ProjectsPayload, + /// `None` when hackd is not reachable. + pub daemon: Option, + /// `None` when doctor was not run in this refresh. + pub doctor: Option, +} + +impl Snapshot { + /// Cache-backed reads refresh quickly; unavailable or mismatched + /// daemon reads retry slowly, without spawning a registry writer. + #[must_use] + pub fn poll_interval(snapshot: Option<&Self>) -> Duration { + if snapshot.is_some_and(|s| { + s.daemon + .as_ref() + .is_some_and(|d| d.version == s.cli_version) + }) { + Duration::from_secs(2) + } else { + Duration::from_secs(10) + } + } + /// hackd runs, but at another version than the CLI. + #[must_use] + pub fn daemon_mismatch(&self) -> Option<&str> { + self.daemon + .as_ref() + .filter(|d| d.version != self.cli_version) + .map(|d| d.version.as_str()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceError { + /// No answer yet (fixtures use it for the loading state). + Pending, + CliMissing, + /// Project reads require a matching daemon until observe-only CLI listing exists. + DaemonUnavailable { + cli: String, + daemon: Option, + message: String, + }, + Cli { + code: String, + message: String, + /// Services the CLI reported as failed to start (`detail.failed`). + failed: Vec, + }, +} + +impl From for SourceError { + fn from(e: RunError) -> Self { + match e { + RunError::NotFound => Self::CliMissing, + RunError::Cli(c) => Self::Cli { + failed: c.failed_services(), + code: c.code, + message: c.message, + }, + other => Self::Cli { + code: "E_APP_RUN".into(), + message: other.to_string(), + failed: vec![], + }, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ActionOutcome { + Lifecycle(LifecycleResult), + Opened(OpenResult), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DaemonRecovery { + Start, + Restart, +} + +impl DaemonRecovery { + #[must_use] + pub fn command(self) -> &'static str { + match self { + Self::Start => "start", + Self::Restart => "restart", + } + } + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Start => "Start hackd", + Self::Restart => "Restart hackd", + } + } +} + +/// Native finite logs are a single-service snapshot (not ndjson), so the +/// combined tail is not available for native projects in v0. +pub const NATIVE_LOGS_NOTE: &str = + "Native logs are read one service at a time; they arrive with the Logs tab."; + +pub trait Source: Send + Sync { + /// Explicit user recovery only; never called by a poll. + /// # Errors + /// Failed or unavailable daemon command. + fn recover_daemon(&self, _action: DaemonRecovery) -> Result<(), String> { + Err("Daemon actions are unavailable for this source".into()) + } + /// # Errors + /// A [`SourceError`] when no usable snapshot could be read. + fn snapshot(&self) -> Result; + + /// Run one action for a project root. `native` selects the native + /// backend (`HACK_RUNTIME_BACKEND=native`), which every native command needs. + /// + /// # Errors + /// A [`SourceError`] from the CLI. + fn act( + &self, + repo_root: &Path, + native: bool, + action: Action, + ) -> Result; + + /// Last log lines for a project (no follow). + /// + /// # Errors + /// Why no tail could be read; shown in place of the tail. + fn log_tail(&self, repo_root: &Path, native: bool, lines: u32) + -> Result, String>; +} + +/// Live data from a matching hackd. CLI listing fallback needs R18: +/// today's `projects` command auto-registers projects and writes the registry. +pub struct LiveSource { + pub runner: CliRunner, + pub daemon: Option, +} + +impl LiveSource { + /// The live source exactly as the app builds it: `hack` resolved on + /// absolute PATH entries, and the hackd socket under `HACK_HOME` + /// (else `~/.hack`). Used by the window and by `hack-desktop-state`. + #[must_use] + pub fn from_env() -> Self { + let socket = crate::daemon::default_socket_path( + std::env::var("HACK_HOME").ok().as_deref(), + std::env::var("HOME").ok().as_deref(), + ); + Self { + runner: CliRunner::on_path("hack"), + daemon: socket.map(|s| DaemonClient::new(s, Duration::from_secs(3))), + } + } + + fn runner_for(&self, native: bool) -> CliRunner { + if native { + self.runner + .clone() + .with_env("HACK_RUNTIME_BACKEND", "native") + } else { + self.runner.clone() + } + } + + const READ: Duration = Duration::from_secs(15); + /// After this long a lifecycle command is only *reported* as slow + /// (`LIFECYCLE_WARN_AFTER`); the app never terminates it. The CLI owns + /// lifecycle timeouts and retires the detached builds it starts; killing + /// it from outside could orphan them (its detached build path does not + /// forward SIGTERM today; requested of the lead). + pub const LIFECYCLE_WARN_AFTER: Duration = Duration::from_secs(6 * 60 * 60); + /// `open` starts nothing; a short limit is safe. + const OPEN: Duration = Duration::from_secs(120); +} + +impl Source for LiveSource { + fn recover_daemon(&self, action: DaemonRecovery) -> Result<(), String> { + let (_, stderr, code) = self + .runner + .run_raw( + &["daemon", action.command()], + None, + Duration::from_secs(120), + ) + .map_err(|e| e.to_string())?; + if code == Some(0) { + Ok(()) + } else { + let exit = code.map_or_else( + || "was killed by a signal".into(), + |n| format!("exited with code {n}"), + ); + Err(format!( + "Could not {} hackd: command {exit}. {}", + action.command(), + stderr.trim() + )) + } + } + fn snapshot(&self) -> Result { + let cli_version = self.runner.version(Duration::from_secs(5))?; + let daemon = self.daemon.as_ref().and_then(|d| d.status().ok()); + let from_daemon = daemon + .as_ref() + .filter(|d| d.version == cli_version) + .and(self.daemon.as_ref()) + .and_then(|d| d.projects().ok()); + let mut projects = from_daemon.ok_or_else(|| SourceError::DaemonUnavailable { + cli: cli_version.clone(), + daemon: daemon.as_ref().map(|d| d.version.clone()), + message: match &daemon { + None => "hackd is off or unreachable. Start hackd to read project status.".into(), + Some(d) if d.version != cli_version => format!( + "hackd v{}, CLI v{cli_version}. Restart hackd to read project status.", + d.version + ), + Some(_) => { + "hackd could not provide project status. Check again or restart hackd.".into() + } + }, + })?; + // Project views do not read native projects; ask native ps for each. + let native = self.runner_for(true); + for row in projects.projects.iter_mut().filter(|r| r.is_native()) { + if let Some(root) = row.repo_root.clone() { + row.native = native + .run_json(&["ps", "--json"], Some(Path::new(&root)), Self::READ) + .ok(); + } + } + Ok(Snapshot { + cli_version, + projects, + daemon, + doctor: None, + }) + } + + fn act( + &self, + repo_root: &Path, + native: bool, + action: Action, + ) -> Result { + // Actions run checkout-defined lifecycle commands as the user: run + // only on a validated root, by its canonical path. + let real = trusted_root(repo_root)?; + let repo_root = real.as_path(); + + let args = action.cli_args(); + let runner = self.runner_for(native); + Ok(match action { + Action::Open => { + ActionOutcome::Opened(runner.run_json(args, Some(repo_root), Self::OPEN)?) + } + // No app deadline: busy until the CLI exits (see LIFECYCLE_WARN_AFTER). + _ => ActionOutcome::Lifecycle(runner.run_json_to_completion(args, Some(repo_root))?), + }) + } + + fn log_tail( + &self, + repo_root: &Path, + native: bool, + lines: u32, + ) -> Result, String> { + if native { + return Err(NATIVE_LOGS_NOTE.into()); + } + let tail = lines.to_string(); + // No --compose / --services: they are backend-specific (DESIGN.md, Logs). + let (out, err, code) = self + .runner + .run_raw( + &["logs", "--json", "--no-follow", "--tail", &tail], + Some(repo_root), + Self::READ, + ) + .map_err(|e| e.to_string())?; + parse_log_tail(&out, &err, code) + } +} + +/// Parse a finite `hack logs --json` run. Any non-zero exit, `error` event or +/// unsuccessful `end` reason (for example `exit:1`) is a failed read, even +/// when some entries arrived: a partial tail is never shown as complete. +/// +/// # Errors +/// The reason the read failed. +pub fn parse_log_tail(out: &str, err: &str, code: Option) -> Result, String> { + let events: Vec = out + .lines() + .filter_map(|l| serde_json::from_str(l).ok()) + .collect(); + if let Some(e) = events.iter().find(|e| e.kind == "error") { + return Err(e + .message + .clone() + .unwrap_or_else(|| "log read failed".into())); + } + let bad_end = events.iter().find(|e| { + e.kind == "end" + && e.reason + .as_deref() + .is_some_and(|r| r == "error" || (r.starts_with("exit:") && r != "exit:0")) + }); + if let Some(e) = bad_end { + return Err(format!( + "log read ended with {}", + e.reason.as_deref().unwrap_or("an error") + )); + } + if code != Some(0) { + return Err(format!("hack logs exited with {code:?}: {}", err.trim())); + } + Ok(events.into_iter().filter_map(|e| e.entry).collect()) +} + +/// Validate a registry-supplied project root before running an action that +/// executes checkout-defined lifecycle commands, and return the canonical +/// path to pass to the CLI. +/// +/// The projects registry is not ownership-checked by the CLI today, so the +/// app refuses a root unless: +/// - it is absolute and canonicalises to an existing directory (the returned +/// canonical path is what runs, so a symlink cannot be swapped between +/// this check and the call); +/// - every ancestor is owned by this user or root and not writable by group +/// or others, except a root-owned sticky directory such as `/tmp`, where +/// other users cannot rename entries; +/// - the root itself is owned by this user and not group/other writable; +/// - the CLI's own discovery, started at the root, selects exactly the root +/// (mirrored by `cli_selects_exactly`), so no other project is chosen; +/// - `.hack/`, `.dev/` and every entry directly in them are plain files or +/// directories (symlinks are refused), owned by this user and not +/// group/other writable. +/// +/// What remains is CLI work: the CLI should verify the registry itself and +/// bind each action to the registered identity (requested of the lead). +/// +/// # Errors +/// `E_APP_UNTRUSTED_ROOT` with the reason. +pub fn trusted_root(root: &Path) -> Result { + use std::os::unix::fs::MetadataExt; + let refuse = |why: String| SourceError::Cli { + code: "E_APP_UNTRUSTED_ROOT".into(), + message: format!("Not running this action: {why}."), + failed: vec![], + }; + // SAFETY: getuid has no preconditions and cannot fail. + let uid = unsafe { libc::getuid() }; + if !root.is_absolute() { + return Err(refuse("the project folder is not an absolute path".into())); + } + let real = std::fs::canonicalize(root) + .map_err(|_| refuse("the project folder could not be read".into()))?; + let meta = std::fs::metadata(&real) + .map_err(|_| refuse("the project folder could not be read".into()))?; + if !meta.is_dir() { + return Err(refuse("the project folder is not a directory".into())); + } + if meta.uid() != uid || meta.mode() & 0o022 != 0 { + return Err(refuse( + "the project folder is not yours alone (owner or write permissions)".into(), + )); + } + for dir in real.ancestors().skip(1) { + let m = std::fs::metadata(dir) + .map_err(|_| refuse(format!("{} could not be read", dir.display())))?; + let owner_ok = m.uid() == uid || m.uid() == 0; + let sticky_root = m.uid() == 0 && m.mode() & 0o1000 != 0; + if !owner_ok || (m.mode() & 0o022 != 0 && !sticky_root) { + return Err(refuse(format!( + "{} can be changed by another user", + dir.display() + ))); + } + } + // Config entries must be plain files and directories (no symlinks: a + // link target's own path chain could be writable by others), owned by + // this user and closed to group and others. + let private = |p: &Path| -> Result<(), SourceError> { + let m = std::fs::symlink_metadata(p) + .map_err(|_| refuse(format!("{} could not be read", p.display())))?; + if m.file_type().is_symlink() { + return Err(refuse(format!("{} is a symbolic link", p.display()))); + } + if m.uid() != uid || m.mode() & 0o022 != 0 { + return Err(refuse(format!( + "{} is not yours alone (owner or write permissions)", + p.display() + ))); + } + Ok(()) + }; + // The CLI must select exactly this root (see `discoverProjectInputs`). + if !cli_selects_exactly(&real) { + return Err(refuse( + "the project folder is not the project the CLI would select from it".into(), + )); + } + for d in [real.join(".hack"), real.join(".dev")] { + if std::fs::symlink_metadata(&d).is_err() { + continue; + } + private(&d)?; + let entries = std::fs::read_dir(&d) + .map_err(|_| refuse(format!("{} could not be read", d.display())))?; + for e in entries { + let e = e.map_err(|_| refuse(format!("{} could not be read", d.display())))?; + private(&e.path())?; + } + } + Ok(real) +} + +/// Mirror of the CLI's project discovery from `root` (see +/// `discoverProjectInputs` in `src/lib/project-input-selection.ts`): `true` +/// only when discovery starting at `root` selects `root` itself. +/// - `.hack/hack.project.json` (native) or `.hack/docker-compose.yml` at +/// `root` selects it; +/// - `.dev/docker-compose.yml` alone selects it unless an ancestor with +/// `.hack/docker-compose.yml` is reached before an ancestor native marker. +fn cli_selects_exactly(root: &Path) -> bool { + let native = |d: &Path| d.join(".hack/hack.project.json").exists(); + let hack_compose = |d: &Path| d.join(".hack/docker-compose.yml").is_file(); + let dev_compose = |d: &Path| d.join(".dev/docker-compose.yml").is_file(); + if native(root) || hack_compose(root) { + return true; + } + if !dev_compose(root) { + return false; + } + for a in root.ancestors().skip(1) { + if native(a) { + return true; + } + if hack_compose(a) { + return false; + } + } + true +} + +/// Canned data for one designed state. +pub struct FixtureSource(pub Fixture); + +impl Source for FixtureSource { + fn snapshot(&self) -> Result { + fixtures::snapshot(self.0) + } + + fn act( + &self, + _repo_root: &Path, + _native: bool, + action: Action, + ) -> Result { + Ok(match action { + Action::Open => ActionOutcome::Opened(OpenResult { + url: "https://storefront.hack".into(), + hint: None, + }), + _ => ActionOutcome::Lifecycle(LifecycleResult::default()), + }) + } + + fn log_tail( + &self, + _repo_root: &Path, + native: bool, + _lines: u32, + ) -> Result, String> { + if native { + return Err(NATIVE_LOGS_NOTE.into()); + } + Ok(fixtures::log_tail(self.0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fake_daemon(projects: &str) -> (DaemonClient, std::thread::JoinHandle<()>) { + use std::io::{Read, Write}; + use std::os::unix::{fs::DirBuilderExt, net::UnixListener}; + let dir = PathBuf::from(format!( + "/tmp/hds-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::DirBuilder::new().mode(0o700).create(&dir).unwrap(); + let socket = dir.join("s"); + let listener = UnixListener::bind(&socket).unwrap(); + listener.set_nonblocking(true).unwrap(); + let projects = projects.to_owned(); + let handle = std::thread::spawn(move || { + for body in [r#"{"status":"ok","version":"5.0.0"}"#.to_owned(), projects] { + let deadline = std::time::Instant::now() + Duration::from_secs(3); + let (mut stream, _) = loop { + match listener.accept() { + Ok(pair) => break pair, + Err(e) + if e.kind() == std::io::ErrorKind::WouldBlock + && std::time::Instant::now() < deadline => + { + std::thread::sleep(Duration::from_millis(5)) + } + Err(e) => panic!("fake daemon accept failed or timed out: {e}"), + } + }; + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut buf = [0; 4096]; + let n = stream.read(&mut buf).unwrap(); + assert!(String::from_utf8_lossy(&buf[..n]).starts_with("GET /v1/")); + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", + body.len(), + body + ) + .unwrap(); + } + std::fs::remove_dir_all(dir).unwrap(); + }); + (DaemonClient::new(socket, Duration::from_secs(2)), handle) + } + + /// A trusted fake daemon provides rows; only native ps uses the fake CLI. + #[test] + fn live_source_reads_native_rows_with_native_backend() { + use std::os::unix::fs::PermissionsExt; + let dir = std::env::temp_dir().join(format!( + "hack-desktop-source-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + let hack = dir.join("hack"); + std::fs::write( + &hack, + r#"#!/bin/sh +case "$1" in + --version) echo "hack v5.0.0" ;; + projects) echo '{"runtime_ok":true,"projects":[{"name":"svc","status":"running","repo_root":"/work/svc"},{"name":"nat","status":"unavailable","repo_root":"/work/nat","input_diagnostic":{"code":"E_NATIVE_PROJECT_UNSUPPORTED","message":"m"}}]}' ;; + ps) if [ "$HACK_RUNTIME_BACKEND" = native ]; then echo '{"ok":true,"data":{"backend":"native","status":"pending","items":[]}}'; else echo '{"ok":false,"error":{"code":"E_NATIVE_PROJECT_UNSUPPORTED","message":"m"}}'; exit 1; fi ;; +esac +"#, + ) + .unwrap(); + std::fs::set_permissions(&hack, std::fs::Permissions::from_mode(0o755)).unwrap(); + // Complete macOS first-launch scanning before the fake daemon's + // bounded accept window starts. --version has no fixture effects. + CliRunner::new(&hack) + .version(Duration::from_secs(60)) + .unwrap(); + let (daemon, server) = fake_daemon( + r#"{"runtime_ok":true,"projects":[{"name":"svc","status":"running","repo_root":"/work/svc"},{"name":"nat","status":"unavailable","repo_root":"/work/nat","backend":"native"}]}"#, + ); + let src = LiveSource { + runner: CliRunner::new(&hack).with_env("HACK_HOME", &dir.to_string_lossy()), + daemon: Some(daemon), + }; + let snap = src.snapshot().unwrap(); + server.join().unwrap(); + let _ = std::fs::remove_dir_all(&dir); + assert!(snap.daemon.is_some()); + let nat = snap + .projects + .projects + .iter() + .find(|p| p.name == "nat") + .unwrap(); + assert_eq!(nat.native.as_ref().unwrap().status, "pending"); + assert_eq!( + crate::state::derive(nat, &crate::state::LocalContext::default()), + crate::state::ProjectState::Starting + ); + let svc = snap + .projects + .projects + .iter() + .find(|p| p.name == "svc") + .unwrap(); + assert!(svc.native.is_none()); + } + + #[test] + fn live_source_rejects_a_payload_without_projects() { + use std::os::unix::fs::PermissionsExt; + let dir = std::env::temp_dir().join(format!( + "hack-desktop-source-empty-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + let hack = dir.join("hack"); + std::fs::write( + &hack, + "#!/bin/sh\ncase \"$1\" in --version) echo 'hack v5.0.0' ;; projects) echo '{}' ;; esac\n", + ) + .unwrap(); + std::fs::set_permissions(&hack, std::fs::Permissions::from_mode(0o755)).unwrap(); + CliRunner::new(&hack) + .version(Duration::from_secs(60)) + .unwrap(); + let (daemon, server) = fake_daemon("{}"); + let src = LiveSource { + runner: CliRunner::new(&hack), + daemon: Some(daemon), + }; + let r = src.snapshot(); + server.join().unwrap(); + let _ = std::fs::remove_dir_all(&dir); + assert!( + matches!(&r, Err(SourceError::DaemonUnavailable { .. })), + "{r:?}" + ); + } + + #[test] + fn failed_log_reads_are_failures_even_with_entries() { + let entry = r#"{"type":"log","entry":{"service":"api","message":"hi"}}"#; + let ok = + format!("{{\"type\":\"start\"}}\n{entry}\n{{\"type\":\"end\",\"reason\":\"eof\"}}\n"); + assert_eq!(parse_log_tail(&ok, "", Some(0)).unwrap().len(), 1); + let bad_end = format!( + "{{\"type\":\"start\"}}\n{entry}\n{{\"type\":\"end\",\"reason\":\"exit:1\"}}\n" + ); + assert!(parse_log_tail(&bad_end, "", Some(0)).is_err()); + assert!(parse_log_tail(&ok, "boom", Some(1)).is_err()); + let err_ev = "{\"type\":\"error\",\"message\":\"no engine\"}\n"; + assert_eq!( + parse_log_tail(err_ev, "", Some(0)).unwrap_err(), + "no engine" + ); + } + + /// Whether the temp dir's own ancestors are private enough for the + /// positive cases. Decided independently of `trusted_root`, so an + /// unexpected rejection fails the test instead of skipping it. + fn host_temp_is_trustworthy() -> bool { + use std::os::unix::fs::MetadataExt; + // SAFETY: getuid has no preconditions and cannot fail. + let uid = unsafe { libc::getuid() }; + let Ok(t) = std::fs::canonicalize(std::env::temp_dir()) else { + return false; + }; + t.ancestors().all(|a| { + std::fs::metadata(a).is_ok_and(|m| { + let sticky_root = m.uid() == 0 && m.mode() & 0o1000 != 0; + (m.uid() == uid || m.uid() == 0) && (m.mode() & 0o022 == 0 || sticky_root) + }) + }) + } + + /// A private base dir under the temp dir. + fn base(tag: &str) -> PathBuf { + use std::os::unix::fs::PermissionsExt; + let dir = std::env::temp_dir().join(format!( + "hack-desktop-trust-{tag}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap(); + dir + } + + /// A real legacy project: `//docker-compose.yml`. + fn make_project(dir: &Path, marker_dir: &str, file: &str) { + use std::os::unix::fs::PermissionsExt; + let d = dir.join(marker_dir); + std::fs::create_dir_all(&d).unwrap(); + std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).unwrap(); + std::fs::set_permissions(&d, std::fs::Permissions::from_mode(0o700)).unwrap(); + std::fs::write(d.join(file), "services: {}\n").unwrap(); + std::fs::set_permissions(d.join(file), std::fs::Permissions::from_mode(0o600)).unwrap(); + } + + fn untrusted(r: Result) -> bool { + matches!(r, Err(SourceError::Cli { ref code, .. }) if code == "E_APP_UNTRUSTED_ROOT") + } + + #[test] + fn trusted_root_accepts_real_projects_and_returns_the_real_path() { + if !host_temp_is_trustworthy() { + eprintln!("skipped: temp dir ancestors are not private on this host"); + return; + } + let b = base("ok"); + let compose = b.join("compose"); + make_project(&compose, ".hack", "docker-compose.yml"); + let native = b.join("native"); + make_project(&native, ".hack", "hack.project.json"); + let dev = b.join("dev"); + make_project(&dev, ".dev", "docker-compose.yml"); + for p in [&compose, &native] { + assert_eq!( + trusted_root(p).unwrap(), + std::fs::canonicalize(p).unwrap(), + "{}", + p.display() + ); + } + // A `.dev`-only project is selected by the CLI unless an ancestor + // `.hack` Compose project comes first; this host may have one above + // its temp dir, in which case the right answer is a refusal. + let real_dev = std::fs::canonicalize(&dev).unwrap(); + let ancestor_hack = real_dev + .ancestors() + .skip(1) + .take_while(|a| !a.join(".hack/hack.project.json").exists()) + .any(|a| a.join(".hack/docker-compose.yml").is_file()); + if ancestor_hack { + assert!(untrusted(trusted_root(&dev)), "CLI would pick the ancestor"); + } else { + assert_eq!(trusted_root(&dev).unwrap(), real_dev); + } + let link = b.join("link"); + std::os::unix::fs::symlink(&compose, &link).unwrap(); + assert_eq!( + trusted_root(&link).unwrap(), + std::fs::canonicalize(&compose).unwrap() + ); + let _ = std::fs::remove_dir_all(&b); + } + + #[test] + fn trusted_root_refuses_unsafe_or_ambiguous_roots() { + use std::os::unix::fs::PermissionsExt; + let set = |p: &Path, m: u32| { + std::fs::set_permissions(p, std::fs::Permissions::from_mode(m)).unwrap(); + }; + assert!(untrusted(trusted_root(Path::new("relative/dir")))); + if !host_temp_is_trustworthy() { + eprintln!("skipped: temp dir ancestors are not private on this host"); + return; + } + let b = base("bad"); + let p = b.join("p"); + make_project(&p, ".hack", "docker-compose.yml"); + assert!(trusted_root(&p).is_ok()); + // World-writable root. + set(&p, 0o777); + assert!(untrusted(trusted_root(&p)), "0777 root"); + set(&p, 0o700); + // Group-writable config file. + set(&p.join(".hack/docker-compose.yml"), 0o664); + assert!(untrusted(trusted_root(&p)), "writable config"); + set(&p.join(".hack/docker-compose.yml"), 0o600); + // A config symlink, even to a private file under a writable parent. + let open = b.join("open"); + std::fs::create_dir(&open).unwrap(); + set(&open, 0o777); + std::fs::write(open.join("target.yml"), "x").unwrap(); + set(&open.join("target.yml"), 0o600); + std::os::unix::fs::symlink(open.join("target.yml"), p.join(".hack/hack.config.json")) + .unwrap(); + assert!(untrusted(trusted_root(&p)), "config symlink"); + std::fs::remove_file(p.join(".hack/hack.config.json")).unwrap(); + // An empty .hack is not a project for the CLI. + let empty = b.join("empty"); + std::fs::create_dir_all(empty.join(".hack")).unwrap(); + set(&empty, 0o700); + set(&empty.join(".hack"), 0o700); + assert!(untrusted(trusted_root(&empty)), "empty .hack"); + // A .dev project below an ancestor .hack project: the CLI picks the + // ancestor, so the nested root is refused. + let outer = b.join("outer"); + make_project(&outer, ".hack", "docker-compose.yml"); + let inner = outer.join("inner"); + make_project(&inner, ".dev", "docker-compose.yml"); + assert!(untrusted(trusted_root(&inner)), "ancestor .hack wins"); + // A child whose parent is group-writable. + let g = b.join("g"); + std::fs::create_dir(&g).unwrap(); + let proj = g.join("proj"); + make_project(&proj, ".hack", "docker-compose.yml"); + set(&g, 0o775); + assert!(untrusted(trusted_root(&proj)), "writable ancestor"); + set(&open, 0o700); + let _ = std::fs::remove_dir_all(&b); + } + + #[test] + fn lifecycle_actions_run_to_completion_without_an_app_deadline() { + // A Start slower than any read timeout still completes; Open keeps + // its bounded limit. (The CLI owns lifecycle timeouts.) + use std::os::unix::fs::PermissionsExt; + let b = base("lifecycle"); + let p = b.join("p"); + make_project(&p, ".hack", "docker-compose.yml"); + let hack = b.join("hack"); + std::fs::write( + &hack, + "#!/bin/sh\nsleep 1\necho '{\"ok\":true,\"data\":{\"action\":\"up\",\"services\":{\"failed\":[]}}}'\n", + ) + .unwrap(); + std::fs::set_permissions(&hack, std::fs::Permissions::from_mode(0o755)).unwrap(); + let src = LiveSource { + runner: CliRunner::new(&hack), + daemon: None, + }; + if host_temp_is_trustworthy() { + let r = src.act(&p, false, Action::Start); + assert!(matches!(r, Ok(ActionOutcome::Lifecycle(_))), "{r:?}"); + } else { + eprintln!("skipped: temp dir ancestors are not private on this host"); + } + let _ = std::fs::remove_dir_all(&b); + } + + #[test] + fn mismatch_detection() { + let s = fixtures::snapshot(Fixture::DaemonMismatch).unwrap(); + assert_eq!(s.daemon_mismatch(), Some("4.3.0")); + let s = fixtures::snapshot(Fixture::Running).unwrap(); + assert_eq!(s.daemon_mismatch(), None); + } + + #[test] + fn run_errors_map_to_source_errors() { + assert_eq!( + SourceError::from(RunError::NotFound), + SourceError::CliMissing + ); + assert!(matches!( + SourceError::from(RunError::Timeout { + after: Duration::from_secs(1) + }), + SourceError::Cli { .. } + )); + } +} diff --git a/apps/desktop/core/src/state.rs b/apps/desktop/core/src/state.rs new file mode 100644 index 000000000..1cabbae13 --- /dev/null +++ b/apps/desktop/core/src/state.rs @@ -0,0 +1,759 @@ +//! Display state of a project, derived from a project row plus what this app +//! knows about its own in-flight action and the last action result. +//! +//! This derivation exists only until the CLI reports a `state` field itself +//! (design request R2). Rules, in order: +//! 1. An action this app is running wins (`Starting` / `Stopping`). +//! 2. Always-on gave up, the checkout is missing, or the config has an input +//! diagnostic → `NeedsAttention`. +//! 3. The last `up` reported failed services, or a defined service exited +//! non-zero and none run → `FailedStart`. +//! 4. Runtime unreadable → `Unknown` (never `Stopped`). +//! 5. Some but not all defined services running, or one unhealthy → +//! `Degraded`; all running → `Running`; none → `Stopped`. + +use crate::models::{NativePs, ProjectRow}; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(tag = "state", rename_all = "snake_case")] +pub enum ProjectState { + Stopped, + Starting, + /// `exited_ok`: services that exited 0 while the rest run. Their + /// completion role is unknown until the CLI reports it (design R2). + Running { + uptime: Option, + exited_ok: Vec, + }, + Degraded { + down: Vec, + }, + Stopping, + FailedStart { + services: Vec, + }, + NeedsAttention { + reason: String, + }, + Unknown { + reason: String, + }, +} + +impl ProjectState { + /// Short label for the state pill. + #[must_use] + pub fn label(&self) -> String { + match self { + Self::Stopped => "Stopped".into(), + Self::Starting => "Starting…".into(), + Self::Running { exited_ok, .. } if !exited_ok.is_empty() => { + format!("Running · {} exited (0)", exited_ok.join(", ")) + } + Self::Running { + uptime: Some(u), .. + } => format!("Running · {u}"), + Self::Running { uptime: None, .. } => "Running".into(), + Self::Degraded { down } => format!("Degraded · {} down", down.join(", ")), + Self::Stopping => "Stopping…".into(), + Self::FailedStart { services } => { + format!("Failed to start: {}", services.join(", ")) + } + Self::NeedsAttention { .. } => "Needs attention".into(), + Self::Unknown { .. } => "Status unknown".into(), + } + } + + /// Sidebar group: running-like states go under "Running". + #[must_use] + pub fn is_active(&self) -> bool { + matches!( + self, + Self::Running { .. } | Self::Degraded { .. } | Self::Starting | Self::Stopping + ) + } + + #[must_use] + pub fn tone(&self) -> Tone { + match self { + Self::Running { .. } => Tone::Ok, + Self::Degraded { .. } => Tone::Warn, + Self::Starting | Self::Stopping => Tone::Busy, + Self::FailedStart { .. } | Self::NeedsAttention { .. } => Tone::Error, + Self::Stopped | Self::Unknown { .. } => Tone::Idle, + } + } + + /// Which actions the header offers in this state. + #[must_use] + pub fn actions(&self) -> &'static [Action] { + match self { + Self::Stopped | Self::Unknown { .. } => &[Action::Start], + Self::Starting => &[Action::Stop], + Self::Running { .. } | Self::Degraded { .. } => { + &[Action::Open, Action::Restart, Action::Stop] + } + Self::Stopping => &[], + Self::FailedStart { .. } | Self::NeedsAttention { .. } => { + &[Action::Start, Action::Stop] + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Tone { + Ok, + Warn, + Busy, + Error, + Idle, +} + +/// v0 project actions. Each maps to exactly one CLI command. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Action { + Start, + Stop, + Restart, + Open, +} + +impl Action { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Start => "Start", + Self::Stop => "Stop", + Self::Restart => "Restart", + Self::Open => "Open", + } + } + + /// CLI arguments (after `hack`), run with `--path `. + #[must_use] + pub fn cli_args(self) -> &'static [&'static str] { + match self { + Self::Start => &["up", "--detach", "--json", "--no-interactive"], + Self::Stop => &["down", "--json", "--no-interactive"], + Self::Restart => &["restart", "--json", "--no-interactive"], + Self::Open => &["open", "--json", "--no-interactive"], + } + } +} + +/// What the app itself knows beyond the row. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct LocalContext { + pub in_flight: Option, + /// Failed services from the last `up`/`restart` this app ran. + pub last_failed: Vec, +} + +/// One service row for the Services table. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct ServiceView { + pub name: String, + pub running: bool, + pub status: String, + pub exit_code: Option, + /// Exited 0 and not running: never "down", but its completion role is + /// unknown until the CLI reports it (shown as "exited (0)"). + pub completed: bool, + /// A container (or native observation) exists for it. A defined service + /// with none may simply be in an inactive Compose profile. + pub created: bool, + pub route: Option, +} + +impl ServiceView { + /// A service with evidence of being down: it has a container that is + /// not running and did not exit 0. A defined service that was never + /// created is not counted: it may sit in an inactive profile, and which + /// services are expected is only known once the CLI reports it (R2). + #[must_use] + pub fn is_down(&self) -> bool { + self.created && !self.running && !self.completed + } +} + +#[must_use] +pub fn services(row: &ProjectRow) -> Vec { + if let Some(ps) = row.native.as_ref().filter(|_| row.is_native()) { + let mut out: Vec = ps + .items + .iter() + .map(|i| { + let running = i.state == "running"; + let status = match &i.health { + Some(h) => format!("{} ({h})", i.state), + None => i.state.clone(), + }; + ServiceView { + created: true, + name: i.service.clone(), + running, + status, + exit_code: i.exit_code, + completed: !running && i.exit_code == Some(0), + route: row + .service_hosts + .get(&i.service) + .and_then(|h| h.first().cloned()), + } + }) + .collect(); + out.sort_by(|a, b| b.is_down().cmp(&a.is_down()).then(a.name.cmp(&b.name))); + return out; + } + let mut names: Vec = row.defined_services.clone(); + if let Some(rt) = &row.runtime { + for s in &rt.services { + if !names.contains(&s.service) { + names.push(s.service.clone()); + } + } + } + let mut out: Vec = names + .into_iter() + .map(|name| { + let containers = row + .runtime + .as_ref() + .and_then(|rt| rt.services.iter().find(|s| s.service == name)) + .map(|s| s.containers.as_slice()) + .unwrap_or_default(); + let running = containers.iter().any(|c| c.is_running()); + let first = containers.first(); + let exit_code = first.and_then(|c| c.exit_code()); + ServiceView { + created: first.is_some(), + completed: !running + && first.is_some_and(|c| c.state.eq_ignore_ascii_case("exited")) + && exit_code == Some(0), + route: row + .service_hosts + .get(&name) + .and_then(|h| h.first().cloned()), + running, + status: first.map_or_else(|| "not created".into(), |c| c.status.clone()), + exit_code, + name, + } + }) + .collect(); + // Down services first, then by name. + out.sort_by(|a, b| b.is_down().cmp(&a.is_down()).then(a.name.cmp(&b.name))); + out +} + +#[must_use] +pub fn derive(row: &ProjectRow, local: &LocalContext) -> ProjectState { + match local.in_flight { + Some(Action::Start | Action::Restart) => return ProjectState::Starting, + Some(Action::Stop) => return ProjectState::Stopping, + Some(Action::Open) | None => {} + } + if let Some(a) = &row.autostart + && a.state == "needs_attention" + { + return ProjectState::NeedsAttention { + reason: format!( + "Always-on stopped trying after {} failures", + a.failures.count + ), + }; + } + if row.status == "missing" { + return ProjectState::NeedsAttention { + reason: "The project folder is missing".into(), + }; + } + if row.is_native() { + return native_state(row.native.as_ref()); + } + if let Some(d) = &row.input_diagnostic { + return ProjectState::NeedsAttention { + reason: d.message.clone(), + }; + } + match row.status.as_str() { + "running" | "stopped" => {} + "unknown" | "unavailable" => { + return ProjectState::Unknown { + reason: "The container runtime could not be read".into(), + }; + } + // Missing or unfamiliar status: no evidence, so not "stopped". + "" => { + return ProjectState::Unknown { + reason: "The CLI reported no status".into(), + }; + } + other => { + return ProjectState::Unknown { + reason: format!("Unrecognised status {other:?}"), + }; + } + } + + let svcs = services(row); + let running: Vec<&ServiceView> = svcs.iter().filter(|s| s.running).collect(); + let mut failed_exit: Vec = svcs + .iter() + .filter(|s| !s.running && s.exit_code.is_some_and(|c| c != 0)) + .map(|s| s.name.clone()) + .collect(); + // Docker's summary counts only `running`. A crash loop or a dead + // container is failure evidence even when the summary says stopped. + let mut uncertain = Vec::new(); + for service in row.runtime.iter().flat_map(|r| &r.services) { + for c in &service.containers { + match c.state.to_ascii_lowercase().as_str() { + "restarting" | "dead" => failed_exit.push(service.service.clone()), + "exited" if c.exit_code().is_some_and(|code| code != 0) => { + failed_exit.push(service.service.clone()) + } + "running" | "exited" => {} + _ => uncertain.push(service.service.clone()), + } + } + } + failed_exit.sort(); + failed_exit.dedup(); + + // The CLI named these as failed: trust that, created or not. + if !local.last_failed.is_empty() + && local + .last_failed + .iter() + .any(|f| !svcs.iter().any(|s| &s.name == f && s.running)) + { + return ProjectState::FailedStart { + services: local.last_failed.clone(), + }; + } + if running.is_empty() { + if !failed_exit.is_empty() { + return ProjectState::FailedStart { + services: failed_exit, + }; + } + if !uncertain.is_empty() { + return ProjectState::Unknown { + reason: format!("Containers are not stopped: {}", uncertain.join(", ")), + }; + } + // Summary rows carry counts, not services. + if row.running_container_count.unwrap_or(0) > 0 || row.status == "running" { + return ProjectState::Running { + uptime: None, + exited_ok: vec![], + }; + } + return ProjectState::Stopped; + } + let unhealthy = row + .runtime + .iter() + .flat_map(|rt| rt.services.iter()) + .filter(|s| s.containers.iter().any(|c| c.is_unhealthy())) + .map(|s| s.service.clone()); + let mut down: Vec = svcs + .iter() + .filter(|s| s.is_down()) + .map(|s| s.name.clone()) + .chain(unhealthy) + .chain(failed_exit) + .chain(uncertain) + .collect(); + down.sort(); + down.dedup(); + if down.is_empty() { + let uptime = row + .runtime + .iter() + .flat_map(|rt| rt.services.iter()) + .flat_map(|s| s.containers.iter()) + .find_map(|c| c.uptime().map(str::to_owned)); + let exited_ok = svcs + .iter() + .filter(|s| s.completed) + .map(|s| s.name.clone()) + .collect(); + ProjectState::Running { uptime, exited_ok } + } else { + ProjectState::Degraded { down } + } +} + +/// Map native `hack ps --json` `data.status` (and items) to a display state. +#[must_use] +pub fn native_state(ps: Option<&NativePs>) -> ProjectState { + let Some(ps) = ps else { + return ProjectState::Unknown { + reason: "Native status was not read".into(), + }; + }; + let unknown = |why: &str| ProjectState::Unknown { + reason: format!("Native runtime: {why}"), + }; + match ps.status.as_str() { + "not_started" => ProjectState::Stopped, + "pending" => ProjectState::Starting, + "runtime_degraded" => ProjectState::Degraded { + down: native_down(ps), + }, + "observed" => { + let down = native_down(ps); + if ps.items.is_empty() { + ProjectState::Stopped + } else if down.is_empty() { + let exited_ok = ps + .items + .iter() + .filter(|i| i.state != "running" && i.exit_code == Some(0)) + .map(|i| i.service.clone()) + .collect(); + ProjectState::Running { + uptime: None, + exited_ok, + } + } else if down.len() == ps.items.len() { + ProjectState::FailedStart { services: down } + } else { + ProjectState::Degraded { down } + } + } + "provider_pin_changed" => ProjectState::NeedsAttention { + reason: "The native runtime provider changed; run hack doctor".into(), + }, + "runtime_not_running" => unknown("not running"), + "runtime_busy" => unknown("busy"), + "owner_unconfirmed" => unknown("owner not confirmed"), + other => unknown(other), + } +} + +fn native_down(ps: &NativePs) -> Vec { + let mut down: Vec = ps + .items + .iter() + .filter(|i| { + let completed = i.readiness.as_deref() == Some("completed") + || (i.exit_code == Some(0) && i.state == "exited"); + (i.state != "running" && !completed) || i.health.as_deref() == Some("unhealthy") + }) + .map(|i| i.service.clone()) + .collect(); + down.sort(); + down.dedup(); + down +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures; + + fn row(name: &str) -> ProjectRow { + fixtures::project(fixtures::Fixture::Running) + .projects + .into_iter() + .find(|p| p.name == name) + .unwrap() + } + + #[test] + fn running_has_uptime() { + let s = derive(&row("storefront"), &LocalContext::default()); + assert_eq!( + s, + ProjectState::Running { + uptime: Some("2 hours".into()), + exited_ok: vec![], + } + ); + assert_eq!(s.actions(), &[Action::Open, Action::Restart, Action::Stop]); + } + + #[test] + fn in_flight_action_wins() { + let local = LocalContext { + in_flight: Some(Action::Stop), + ..LocalContext::default() + }; + assert_eq!(derive(&row("storefront"), &local), ProjectState::Stopping); + } + + #[test] + fn unfamiliar_or_missing_status_is_unknown() { + for status in ["", "unregistered", "something_new"] { + let mut r = row("storefront"); + r.status = status.into(); + assert!( + matches!( + derive(&r, &LocalContext::default()), + ProjectState::Unknown { .. } + ), + "{status:?}" + ); + } + } + + #[test] + fn unknown_is_not_stopped() { + let mut r = row("storefront"); + r.status = "unknown".into(); + r.runtime = None; + assert!(matches!( + derive(&r, &LocalContext::default()), + ProjectState::Unknown { .. } + )); + } + + #[test] + fn every_fixture_state_derives_as_designed() { + use fixtures::Fixture::*; + let cases = [ + (Stopped, "Stopped"), + (Running, "Running · 2 hours"), + (Degraded, "Degraded · worker down"), + (FailedStart, "Failed to start: api, worker"), + (NeedsAttention, "Needs attention"), + ]; + for (fx, label) in cases { + let p = fixtures::project(fx); + let sel = fixtures::selected(fx); + let r = p.projects.iter().find(|r| r.name == sel).unwrap(); + assert_eq!( + derive(r, &fixtures::local(fx)).label(), + label, + "fixture {fx:?}" + ); + } + let s = fixtures::project(Starting); + let r = s + .projects + .iter() + .find(|r| r.name == fixtures::selected(Starting)) + .unwrap(); + assert_eq!( + derive(r, &fixtures::local(Starting)), + ProjectState::Starting + ); + } + + #[test] + fn completed_one_shot_is_not_down() { + let mut r = row("storefront"); + let rt = r.runtime.as_mut().unwrap(); + rt.services[1].containers[0].state = "exited".into(); + rt.services[1].containers[0].status = "Exited (0) 2 hours ago".into(); + let st = derive(&r, &LocalContext::default()); + assert_eq!(st.label(), "Running · db exited (0)"); + assert!(matches!(st, ProjectState::Running { .. })); + } + + #[test] + fn native_status_mapping() { + use crate::models::{NativePs, NativePsItem}; + let ps = |status: &str, items: Vec<(&str, &str)>| NativePs { + status: status.into(), + items: items + .into_iter() + .map(|(s, st)| NativePsItem { + service: s.into(), + state: st.into(), + ..NativePsItem::default() + }) + .collect(), + }; + assert_eq!( + native_state(Some(&ps("not_started", vec![]))), + ProjectState::Stopped + ); + assert_eq!( + native_state(Some(&ps("pending", vec![]))), + ProjectState::Starting + ); + assert!(matches!( + native_state(Some(&ps("observed", vec![("app", "running")]))), + ProjectState::Running { .. } + )); + assert_eq!( + native_state(Some(&ps( + "observed", + vec![("app", "running"), ("job", "exited")] + ))), + ProjectState::Degraded { + down: vec!["job".into()] + } + ); + assert!(matches!( + native_state(Some(&ps("runtime_busy", vec![]))), + ProjectState::Unknown { .. } + )); + assert!(matches!(native_state(None), ProjectState::Unknown { .. })); + assert!(matches!( + native_state(Some(&ps("provider_pin_changed", vec![]))), + ProjectState::NeedsAttention { .. } + )); + } + + #[test] + fn native_rows_show_native_services_and_exited_ok() { + use crate::models::{InputDiagnostic, NativePs, NativePsItem, ProjectRow}; + let row = ProjectRow { + name: "n".into(), + status: "unavailable".into(), + input_diagnostic: Some(InputDiagnostic { + code: crate::models::NATIVE_PROJECT_CODE.into(), + message: "m".into(), + }), + native: Some(NativePs { + status: "observed".into(), + items: vec![ + NativePsItem { + service: "web".into(), + state: "running".into(), + health: Some("healthy".into()), + ..NativePsItem::default() + }, + NativePsItem { + service: "migrate".into(), + state: "exited".into(), + exit_code: Some(0), + ..NativePsItem::default() + }, + ], + }), + ..ProjectRow::default() + }; + let svcs = services(&row); + assert_eq!(svcs.len(), 2); + assert_eq!( + svcs.iter().find(|s| s.name == "web").unwrap().status, + "running (healthy)" + ); + assert!(svcs.iter().find(|s| s.name == "migrate").unwrap().completed); + assert_eq!( + derive(&row, &LocalContext::default()).label(), + "Running · migrate exited (0)" + ); + } + + #[test] + fn inactive_profile_services_are_not_down() { + // `toolchain` is defined (another profile) but has no container. + let mut r = row("storefront"); + r.defined_services.push("toolchain".into()); + let st = derive(&r, &LocalContext::default()); + assert!(matches!(st, ProjectState::Running { .. }), "{st:?}"); + let svc = services(&r) + .into_iter() + .find(|s| s.name == "toolchain") + .unwrap(); + assert!(!svc.created && !svc.is_down()); + assert_eq!(svc.status, "not created"); + } + + #[test] + fn services_sort_down_first() { + let p = fixtures::project(fixtures::Fixture::Degraded); + let r = p.projects.iter().find(|r| r.name == "storefront").unwrap(); + let s = services(r); + assert_eq!(s[0].name, "worker"); + assert!(!s[0].running); + assert_eq!(s[1].route.as_deref(), Some("api.storefront.hack")); + } + #[test] + fn docker_non_running_states_are_not_stopped() { + for (state, status, failed) in [ + ("restarting", "Restarting (1) 5 seconds ago", true), + ("dead", "Dead", true), + ("created", "Created", false), + ("paused", "Up 2 minutes (Paused)", false), + ] { + let row: ProjectRow = serde_json::from_value(serde_json::json!({ + "name": "sample", "status": "stopped", "defined_services": ["api"], + "runtime": {"services": [{"service":"api", "containers":[{"state":state,"status":status}]}]} + })).unwrap(); + let observed = derive(&row, &LocalContext::default()); + if failed { + assert_eq!( + observed, + ProjectState::FailedStart { + services: vec!["api".into()] + }, + "{state}" + ); + } else { + assert!( + matches!(observed, ProjectState::Unknown { .. }), + "{state}: {observed:?}" + ); + } + assert!(!services(&row)[0].completed); + } + } + + #[test] + fn unhealthy_replica_is_degraded_even_when_another_replica_runs() { + let row: ProjectRow = serde_json::from_value(serde_json::json!({ + "name":"sample", "status":"running", "defined_services":["api"], + "runtime":{"services":[{"service":"api","containers":[ + {"state":"running","status":"Up 2 minutes"}, + {"state":"restarting","status":"Restarting (1) 5 seconds ago"} + ]}]} + })) + .unwrap(); + assert_eq!( + derive(&row, &LocalContext::default()), + ProjectState::Degraded { + down: vec!["api".into()] + } + ); + } + + #[test] + fn any_failed_exited_replica_prevents_a_healthy_or_stopped_project() { + for (first_state, first_status, expected) in [ + ( + "running", + "Up 2 minutes", + ProjectState::Degraded { + down: vec!["api".into()], + }, + ), + ( + "exited", + "Exited (0) 1 minute ago", + ProjectState::FailedStart { + services: vec!["api".into()], + }, + ), + ] { + for reversed in [false, true] { + let mut containers = vec![ + serde_json::json!({"state":first_state,"status":first_status}), + serde_json::json!({"state":"exited","status":"Exited (1) 30 seconds ago"}), + ]; + if reversed { + containers.reverse(); + } + let row: ProjectRow = serde_json::from_value(serde_json::json!({ + "name":"sample", "status":if first_state == "running" { "running" } else { "stopped" }, + "defined_services":["api"], "runtime":{"services":[{"service":"api","containers":containers}]} + })).unwrap(); + assert_eq!( + derive(&row, &LocalContext::default()), + expected, + "{first_state}, reversed={reversed}" + ); + } + } + } +} diff --git a/apps/desktop/core/src/viewmodel.rs b/apps/desktop/core/src/viewmodel.rs new file mode 100644 index 000000000..9cc66a8d7 --- /dev/null +++ b/apps/desktop/core/src/viewmodel.rs @@ -0,0 +1,904 @@ +//! View-model state machines for the project list and the detail pane. +//! +//! Pure: no I/O, no clock. The app feeds events in and renders `view()`. +//! +//! List rules (DESIGN.md, "Project list"): +//! - A failed refresh keeps the last good snapshot and marks it stale; with +//! no snapshot yet it shows the error state. +//! - Banners stack: daemon off, daemon version mismatch, engine down, stale. +//! - One action per project at a time; the row's state shows it. + +use std::collections::HashMap; + +use crate::models::ProjectRow; +use crate::source::{ActionOutcome, DaemonRecovery, Snapshot, SourceError}; +use crate::state::{self, Action, LocalContext, ProjectState, ServiceView}; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(tag = "banner", rename_all = "snake_case")] +pub enum Banner { + DaemonOff, + DaemonMismatch { daemon: String, cli: String }, + DaemonReadFailed { error: String }, + EngineDown { error: String }, + Stale { error: String }, +} + +impl Banner { + #[must_use] + pub fn text(&self) -> String { + match self { + Self::DaemonOff => { + "hackd is off or unreachable: run `hack daemon start` to read project status".into() + } + Self::DaemonMismatch { daemon, cli } => { + format!("hackd v{daemon} is running, CLI is v{cli}: restart hackd") + } + Self::EngineDown { error } => format!("Docker not reachable: {error}"), + Self::DaemonReadFailed { error } => error.clone(), + Self::Stale { error } => format!("Showing last known state: {error}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(tag = "phase", rename_all = "snake_case")] +pub enum Phase { + Loading, + CliMissing, + Error { code: String, message: String }, + Empty, + Ready, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct Row { + pub name: String, + pub state: ProjectState, + pub always_on: bool, + pub selected: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct ListView { + pub phase: Phase, + pub banners: Vec, + pub running: Vec, + pub stopped: Vec, + pub doctor_count: usize, + /// Doctor results exist in this snapshot. Until then the Doctor row is + /// unknown (grey), never "no findings" (green). + pub doctor_known: bool, + pub cli_version: Option, + /// Counts by state for the status bar; unknown is never "stopped". + pub counts: Counts, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)] +pub struct Counts { + pub running: usize, + pub stopped: usize, + pub unknown: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct DetailView { + pub name: String, + pub state: ProjectState, + pub backend: String, + pub url: Option, + pub always_on: bool, + pub actions: Vec, + pub busy: bool, + pub services: Vec, + pub last_error: Option, + /// Why actions are limited (native projects in v0). + pub note: Option, +} + +/// v0 scope: native projects are view-only. Native `restart` (and authored +/// `down`/`restart`) stay in the foreground and refuse `--json`; native +/// `up --json` is detached, but the app waits for one contract (design R2b). +pub const NATIVE_ACTIONS_NOTE: &str = "Native projects are view-only in this preview: start, stop or restart them in a terminal with HACK_RUNTIME_BACKEND=native hack up / down / restart."; + +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + Refreshed(Result), + Select(String), + ActionStarted { + project: String, + action: Action, + }, + ActionFinished { + project: String, + action: Action, + result: Result, + }, +} + +/// Identity of a read request, captured before any I/O starts. +#[derive(Debug, Clone, Copy)] +pub struct RefreshToken { + sequence: u64, + action_epoch: u64, +} + +#[derive(Debug, Default)] +pub struct AppModel { + snapshot: Option, + last_error: Option, + selected: Option, + local: HashMap, + action_errors: HashMap, + loaded_once: bool, + refresh_sequence: u64, + applied_sequence: u64, + action_epoch: u64, +} + +impl AppModel { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Start a read. Results from before an action boundary or a newer + /// applied read cannot replace current state or prove failure recovery. + pub fn begin_refresh(&mut self) -> RefreshToken { + self.refresh_sequence += 1; + RefreshToken { + sequence: self.refresh_sequence, + action_epoch: self.action_epoch, + } + } + + #[must_use] + pub fn poll_interval(&self) -> std::time::Duration { + if self.last_error.is_some() { + std::time::Duration::from_secs(10) + } else { + Snapshot::poll_interval(self.snapshot.as_ref()) + } + } + + #[must_use] + pub fn daemon_recovery(&self) -> Option { + match &self.last_error { + Some(SourceError::DaemonUnavailable { daemon: None, .. }) => { + Some(DaemonRecovery::Start) + } + Some(SourceError::DaemonUnavailable { .. }) => Some(DaemonRecovery::Restart), + _ => None, + } + } + + #[must_use] + pub fn engine_observed(&self) -> bool { + self.last_error.is_none() + && self + .snapshot + .as_ref() + .is_some_and(|s| s.projects.runtime_ok.is_some()) + } + + fn observed_state(&self, row: &ProjectRow, local: &LocalContext) -> ProjectState { + if let Some(error) = &self.last_error { + return ProjectState::Unknown { + reason: describe(error), + }; + } + state::derive(row, local) + } + + /// Apply an ordered live read; returns whether it was accepted. + pub fn finish_refresh( + &mut self, + token: RefreshToken, + result: Result, + ) -> bool { + if token.action_epoch != self.action_epoch || token.sequence <= self.applied_sequence { + return false; + } + self.applied_sequence = token.sequence; + self.apply(Event::Refreshed(result)); + true + } + + /// Apply one event. Returns `true` when the app should refresh now. + pub fn apply(&mut self, event: Event) -> bool { + match event { + Event::Refreshed(Ok(s)) => { + self.loaded_once = true; + self.clear_recovered_failures(&s); + self.last_error = None; + if self.selected.is_none() + || !s + .projects + .projects + .iter() + .any(|p| Some(&p.name) == self.selected.as_ref()) + { + self.selected = first_by_group(&s, &self.local); + } + self.snapshot = Some(s); + false + } + Event::Refreshed(Err(SourceError::Pending)) => false, + Event::Refreshed(Err(e)) => { + self.loaded_once = true; + self.last_error = Some(e); + false + } + Event::Select(name) => { + self.selected = Some(name); + false + } + Event::ActionStarted { project, action } => { + let ctx = self.local.entry(project.clone()).or_default(); + if ctx.in_flight.is_some() { + return false; + } + ctx.in_flight = Some(action); + self.action_epoch += 1; + self.action_errors.remove(&project); + false + } + Event::ActionFinished { + project, + action, + result, + } => { + self.action_epoch += 1; + let ctx = self.local.entry(project.clone()).or_default(); + if ctx.in_flight == Some(action) { + ctx.in_flight = None; + } + match result { + Ok(ActionOutcome::Lifecycle(r)) => { + ctx.last_failed = if matches!(action, Action::Start | Action::Restart) { + r.services.failed + } else { + vec![] + }; + } + Ok(ActionOutcome::Opened(_)) => {} + Err(e) => { + if let SourceError::Cli { failed, .. } = &e + && matches!(action, Action::Start | Action::Restart) + { + ctx.last_failed.clone_from(failed); + } + self.action_errors.insert(project, describe(&e)); + } + } + action != Action::Open + } + } + } + + /// A startup failure is cleared only on evidence that it is over: every + /// failed service is observed running again. (A successful Stop from + /// this app clears it too, in `ActionFinished`.) "Nothing runs" is not + /// evidence: a failed start can leave containers never created. + fn clear_recovered_failures(&mut self, s: &Snapshot) { + for p in &s.projects.projects { + let Some(ctx) = self.local.get_mut(&p.name) else { + continue; + }; + if ctx.last_failed.is_empty() || ctx.in_flight.is_some() { + continue; + } + let svcs = state::services(p); + let recovered = ctx + .last_failed + .iter() + .all(|f| svcs.iter().any(|s| &s.name == f && s.running)); + if recovered { + ctx.last_failed.clear(); + } + } + } + + /// Can `action` start for `project` now? + #[must_use] + pub fn can_start(&self, project: &str) -> bool { + self.last_error.is_none() + && self + .local + .get(project) + .is_none_or(|c| c.in_flight.is_none()) + } + + #[must_use] + pub fn selected_row(&self) -> Option<&ProjectRow> { + let s = self.snapshot.as_ref()?; + let name = self.selected.as_ref()?; + s.projects.projects.iter().find(|p| &p.name == name) + } + + #[must_use] + pub fn list(&self) -> ListView { + let mut banners = Vec::new(); + let phase = match (&self.snapshot, &self.last_error) { + (_, Some(SourceError::CliMissing)) => Phase::CliMissing, + (None, Some(SourceError::Cli { code, message, .. })) => Phase::Error { + code: code.clone(), + message: message.clone(), + }, + (None, Some(SourceError::DaemonUnavailable { message, .. })) => Phase::Error { + code: "E_APP_DAEMON_UNAVAILABLE".into(), + message: message.clone(), + }, + (None, _) => Phase::Loading, + (Some(s), _) if s.projects.projects.is_empty() => Phase::Empty, + (Some(_), _) => Phase::Ready, + }; + let mut running = Vec::new(); + let mut stopped = Vec::new(); + if let Some(SourceError::DaemonUnavailable { + cli, + daemon, + message, + }) = &self.last_error + { + banners.push(match daemon { + None => Banner::DaemonOff, + Some(d) if d != cli => Banner::DaemonMismatch { + daemon: d.clone(), + cli: cli.clone(), + }, + Some(_) => Banner::DaemonReadFailed { + error: message.clone(), + }, + }); + } + if let Some(s) = &self.snapshot { + if !matches!(self.last_error, Some(SourceError::DaemonUnavailable { .. })) { + match (&s.daemon, s.daemon_mismatch()) { + (None, _) => banners.push(Banner::DaemonOff), + (Some(_), Some(v)) => banners.push(Banner::DaemonMismatch { + daemon: v.to_owned(), + cli: s.cli_version.clone(), + }), + _ => {} + } + } + if s.projects.engine_down() { + banners.push(Banner::EngineDown { + error: s.projects.runtime_error.clone().unwrap_or_default(), + }); + } + if let Some(e) = &self.last_error { + banners.push(Banner::Stale { error: describe(e) }); + } + for p in &s.projects.projects { + let row = Row { + state: self.observed_state( + p, + self.local.get(&p.name).unwrap_or(&LocalContext::default()), + ), + always_on: p.autostart.as_ref().is_some_and(|a| a.enabled), + selected: Some(&p.name) == self.selected.as_ref(), + name: p.name.clone(), + }; + if row.state.is_active() { + running.push(row); + } else { + stopped.push(row); + } + } + } + running.sort_by(|a, b| a.name.cmp(&b.name)); + stopped.sort_by(|a, b| a.name.cmp(&b.name)); + let unknown = stopped + .iter() + .filter(|r| matches!(r.state, ProjectState::Unknown { .. })) + .count(); + let counts = Counts { + running: running.len(), + stopped: stopped.len() - unknown, + unknown, + }; + ListView { + phase, + banners, + running, + stopped, + doctor_known: self.snapshot.as_ref().is_some_and(|s| s.doctor.is_some()), + doctor_count: self + .snapshot + .as_ref() + .and_then(|s| s.doctor.as_ref()) + .map_or(0, crate::models::DoctorReport::attention_count), + cli_version: self.snapshot.as_ref().map(|s| s.cli_version.clone()), + counts, + } + } + + #[must_use] + pub fn detail(&self) -> Option { + let row = self.selected_row()?; + let default = LocalContext::default(); + let local = self.local.get(&row.name).unwrap_or(&default); + let st = self.observed_state(row, local); + let mut services = state::services(row); + if self.last_error.is_some() { + for service in &mut services { + service.running = false; + service.completed = false; + service.exit_code = None; + service.status = "Status unknown".into(); + } + } + Some(DetailView { + name: row.name.clone(), + backend: if row.is_native() { + "native".into() + } else { + row.backend.clone().unwrap_or_else(|| "compose".into()) + }, + url: row.dev_host.as_ref().map(|h| format!("https://{h}")), + always_on: row.autostart.as_ref().is_some_and(|a| a.enabled), + actions: if row.is_native() || self.last_error.is_some() { + vec![] + } else { + st.actions().to_vec() + }, + note: row.is_native().then(|| NATIVE_ACTIONS_NOTE.to_owned()), + busy: local.in_flight.is_some(), + services, + last_error: self.action_errors.get(&row.name).cloned(), + state: st, + }) + } +} + +fn first_by_group(s: &Snapshot, local: &HashMap) -> Option { + let default = LocalContext::default(); + let mut rows: Vec<(bool, &str)> = s + .projects + .projects + .iter() + .map(|p| { + let st = state::derive(p, local.get(&p.name).unwrap_or(&default)); + (!st.is_active(), p.name.as_str()) + }) + .collect(); + rows.sort(); + rows.first().map(|(_, n)| (*n).to_owned()) +} + +fn describe(e: &SourceError) -> String { + match e { + SourceError::Pending => "still loading".into(), + SourceError::CliMissing => "hack was not found on PATH".into(), + SourceError::DaemonUnavailable { message, .. } => message.clone(), + SourceError::Cli { code, message, .. } => format!("{code}: {message}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures::{self, Fixture}; + use crate::models::LifecycleResult; + + #[test] + fn unavailable_daemon_invalidates_rows_and_offers_exact_recovery() { + for (daemon, expected) in [ + (None, DaemonRecovery::Start), + (Some("4.0.0".into()), DaemonRecovery::Restart), + ] { + let mut m = model(Fixture::Running); + assert!(m.engine_observed()); + m.apply(Event::Select("storefront".into())); + let token = m.begin_refresh(); + let message = if daemon.is_some() { + "hackd v4.0.0, CLI v5.0.0: restart hackd" + } else { + "hackd is off: start hackd" + }; + m.finish_refresh( + token, + Err(SourceError::DaemonUnavailable { + cli: "5.0.0".into(), + daemon, + message: message.into(), + }), + ); + let list = m.list(); + assert!(list.running.is_empty()); + assert_eq!(list.counts.unknown, 4); + assert_eq!(list.counts.stopped, 0); + assert!(list.stopped.iter().all( + |r| matches!(&r.state, ProjectState::Unknown { reason } if reason.contains("hackd")) + )); + assert!(list.banners.iter().any(|b| match expected { + DaemonRecovery::Start => matches!(b, Banner::DaemonOff), + DaemonRecovery::Restart => matches!(b, Banner::DaemonMismatch { .. }), + })); + assert_eq!(m.daemon_recovery(), Some(expected)); + assert!(m.detail().unwrap().actions.is_empty()); + assert!(!m.can_start("storefront")); + assert!(!m.engine_observed()); + assert!( + m.detail() + .unwrap() + .services + .iter() + .all(|s| !s.running && s.status == "Status unknown") + ); + let refreshed = m.begin_refresh(); + m.finish_refresh(refreshed, fixtures::snapshot(Fixture::Running)); + assert!(m.engine_observed()); + assert!(m.can_start("storefront")); + assert!(!m.detail().unwrap().actions.is_empty()); + } + let mut first_launch = AppModel::new(); + first_launch.apply(Event::Refreshed(Err(SourceError::DaemonUnavailable { + cli: "5.0.0".into(), + daemon: None, + message: "hackd is off".into(), + }))); + assert_eq!(first_launch.daemon_recovery(), Some(DaemonRecovery::Start)); + assert!(matches!(first_launch.list().phase, Phase::Error { .. })); + } + + #[test] + fn stale_reads_cannot_erase_startup_failure() { + let mut m = model(Fixture::Running); + m.apply(Event::Select("storefront".into())); + let before_start = m.begin_refresh(); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Restart, + }); + let during_action = m.begin_refresh(); + m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Restart, + result: Err(SourceError::Cli { + code: "E_STARTUP_INCOMPLETE".into(), + message: "Could not start api".into(), + failed: vec!["api".into()], + }), + }); + assert!(!m.finish_refresh(before_start, fixtures::snapshot(Fixture::Running))); + assert!(!m.finish_refresh(during_action, fixtures::snapshot(Fixture::Running))); + let fresh = m.begin_refresh(); + let mut snapshot = fixtures::snapshot(Fixture::Running).unwrap(); + let project = snapshot + .projects + .projects + .iter_mut() + .find(|p| p.name == "storefront") + .unwrap(); + project + .runtime + .as_mut() + .unwrap() + .services + .retain(|s| s.service != "api"); + assert!(m.finish_refresh(fresh, Ok(snapshot))); + assert_eq!( + m.detail().unwrap().state, + ProjectState::FailedStart { + services: vec!["api".into()] + } + ); + let recovered = m.begin_refresh(); + assert!(m.finish_refresh(recovered, fixtures::snapshot(Fixture::Running))); + assert!(matches!( + m.detail().unwrap().state, + ProjectState::Running { .. } + )); + } + + #[test] + fn last_applied_request_wins_and_failed_reads_retry_slowly() { + let mut m = model(Fixture::Running); + let old = m.begin_refresh(); + let new = m.begin_refresh(); + assert!(m.finish_refresh(new, fixtures::snapshot(Fixture::Stopped))); + assert!(!m.finish_refresh(old, fixtures::snapshot(Fixture::Running))); + assert_eq!(m.poll_interval(), std::time::Duration::from_secs(2)); + let failed = m.begin_refresh(); + m.finish_refresh(failed, fixtures::snapshot(Fixture::Error)); + assert_eq!(m.poll_interval(), std::time::Duration::from_secs(10)); + assert_eq!( + Snapshot::poll_interval(None), + std::time::Duration::from_secs(10) + ); + } + + fn model(f: Fixture) -> AppModel { + let mut m = AppModel::new(); + m.apply(Event::Refreshed(fixtures::snapshot(f))); + m + } + + #[test] + fn list_phases_for_every_list_fixture() { + assert_eq!(model(Fixture::Loading).list().phase, Phase::Loading); + assert_eq!(model(Fixture::Empty).list().phase, Phase::Empty); + assert_eq!(model(Fixture::CliMissing).list().phase, Phase::CliMissing); + assert!(matches!( + model(Fixture::Error).list().phase, + Phase::Error { .. } + )); + assert_eq!( + model(Fixture::DaemonDown).list().banners, + vec![Banner::DaemonOff] + ); + assert!(matches!( + model(Fixture::DaemonMismatch).list().banners[..], + [Banner::DaemonMismatch { .. }] + )); + let engine = model(Fixture::EngineDown).list(); + assert!(matches!(engine.banners[..], [Banner::EngineDown { .. }])); + assert!(engine.running.is_empty(), "unknown is not running"); + assert_eq!(engine.counts.stopped, 0, "unknown is not stopped"); + assert_eq!(engine.counts.unknown, 4); + } + + #[test] + fn groups_and_selection() { + let v = model(Fixture::Running).list(); + let names = |rows: &[Row]| rows.iter().map(|r| r.name.clone()).collect::>(); + assert_eq!(names(&v.running), ["docs-site", "storefront"]); + assert_eq!(names(&v.stopped), ["billing-api", "notes"]); + assert!(v.running[0].selected, "first running project selected"); + assert!(v.running[0].always_on); + assert_eq!(v.doctor_count, 1); + assert!(v.doctor_known); + } + + #[test] + fn doctor_is_unknown_until_it_has_run() { + let mut snap = fixtures::snapshot(Fixture::Running).unwrap(); + snap.doctor = None; // live snapshots carry no doctor results + let mut m = AppModel::new(); + m.apply(Event::Refreshed(Ok(snap))); + let v = m.list(); + assert!(!v.doctor_known); + assert_eq!(v.doctor_count, 0); + } + + #[test] + fn failed_refresh_keeps_last_good_and_marks_stale() { + let mut m = model(Fixture::Running); + m.apply(Event::Refreshed(Err(SourceError::Cli { + code: "E_UNEXPECTED".into(), + message: "boom".into(), + failed: vec![], + }))); + let v = m.list(); + assert_eq!(v.phase, Phase::Ready); + assert!(matches!(v.banners.last(), Some(Banner::Stale { .. }))); + assert!(v.running.is_empty()); + assert_eq!(v.counts.unknown, 4); + assert!(m.detail().unwrap().actions.is_empty()); + } + + #[test] + fn action_lifecycle_and_single_flight() { + let mut m = model(Fixture::Stopped); + m.apply(Event::Select("storefront".into())); + assert_eq!(m.detail().unwrap().actions, vec![Action::Start]); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Start, + }); + let d = m.detail().unwrap(); + assert_eq!(d.state, ProjectState::Starting); + assert!(d.busy); + assert!(!m.can_start("storefront")); + // A second action while one runs is ignored. + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Restart, + }); + let refresh = m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Start, + result: Ok(ActionOutcome::Lifecycle(LifecycleResult { + services: crate::models::LifecycleServices { + failed: vec!["api".into()], + ..Default::default() + }, + ..Default::default() + })), + }); + assert!(refresh, "lifecycle actions refresh at once"); + assert_eq!( + m.detail().unwrap().state, + ProjectState::FailedStart { + services: vec!["api".into()] + } + ); + } + + #[test] + fn open_is_single_flight_too() { + let mut m = model(Fixture::Running); + m.apply(Event::Select("storefront".into())); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Open, + }); + assert!(!m.can_start("storefront")); + assert!(m.detail().unwrap().busy); + assert!(matches!( + m.detail().unwrap().state, + ProjectState::Running { .. } + )); + } + + #[test] + fn startup_failure_error_names_failed_services() { + let mut m = model(Fixture::Running); + m.apply(Event::Select("storefront".into())); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Restart, + }); + m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Restart, + result: Err(SourceError::Cli { + code: "E_STARTUP_INCOMPLETE".into(), + message: "m".into(), + failed: vec!["worker".into()], + }), + }); + // Fixture still shows all running; the failure shows once a service is down. + assert!(m.detail().unwrap().last_error.is_some()); + let mut d = model(Fixture::Degraded); + d.apply(Event::Select("storefront".into())); + d.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Start, + }); + d.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Start, + result: Err(SourceError::Cli { + code: "E_STARTUP_INCOMPLETE".into(), + message: "m".into(), + failed: vec!["worker".into()], + }), + }); + assert_eq!( + d.detail().unwrap().state, + ProjectState::FailedStart { + services: vec!["worker".into()] + } + ); + } + + #[test] + fn recovered_failure_does_not_stay_latched() { + let mut m = model(Fixture::Degraded); + m.apply(Event::Select("storefront".into())); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Start, + }); + m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Start, + result: Err(SourceError::Cli { + code: "E_STARTUP_INCOMPLETE".into(), + message: "m".into(), + failed: vec!["worker".into()], + }), + }); + assert!(matches!( + m.detail().unwrap().state, + ProjectState::FailedStart { .. } + )); + // Recovered elsewhere: everything runs. + m.apply(Event::Refreshed(fixtures::snapshot(Fixture::Running))); + assert!(matches!( + m.detail().unwrap().state, + ProjectState::Running { .. } + )); + // Then stopped elsewhere: plain Stopped, not "Failed to start". + m.apply(Event::Refreshed(fixtures::snapshot(Fixture::Stopped))); + assert_eq!(m.detail().unwrap().state, ProjectState::Stopped); + } + + #[test] + fn failed_start_with_nothing_created_stays_failed() { + let mut m = model(Fixture::Stopped); + m.apply(Event::Select("storefront".into())); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Start, + }); + m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Start, + result: Err(SourceError::Cli { + code: "E_STARTUP_INCOMPLETE".into(), + message: "m".into(), + failed: vec!["api".into()], + }), + }); + let failed = ProjectState::FailedStart { + services: vec!["api".into()], + }; + assert_eq!(m.detail().unwrap().state, failed); + // The same stopped observation is no evidence of recovery. + m.apply(Event::Refreshed(fixtures::snapshot(Fixture::Stopped))); + assert_eq!(m.detail().unwrap().state, failed); + // An explicit successful Stop clears it. + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Stop, + }); + m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Stop, + result: Ok(ActionOutcome::Lifecycle(LifecycleResult::default())), + }); + assert_eq!(m.detail().unwrap().state, ProjectState::Stopped); + } + + #[test] + fn native_projects_offer_no_actions_and_say_why() { + let mut m = model(Fixture::Running); + m.apply(Event::Select("docs-site".into())); + let d = m.detail().unwrap(); + assert!(d.actions.is_empty()); + assert!(d.note.is_some()); + assert_eq!(d.backend, "native"); + } + + #[test] + fn action_error_is_shown_on_the_project() { + let mut m = model(Fixture::Running); + m.apply(Event::Select("storefront".into())); + m.apply(Event::ActionStarted { + project: "storefront".into(), + action: Action::Stop, + }); + m.apply(Event::ActionFinished { + project: "storefront".into(), + action: Action::Stop, + result: Err(SourceError::Cli { + code: "E_DOCKER_UNAVAILABLE".into(), + message: "down".into(), + failed: vec![], + }), + }); + let d = m.detail().unwrap(); + assert_eq!(d.last_error.as_deref(), Some("E_DOCKER_UNAVAILABLE: down")); + assert!(!d.busy); + } + + #[test] + fn every_detail_fixture_renders_a_detail() { + for f in [ + Fixture::Stopped, + Fixture::Starting, + Fixture::Running, + Fixture::Degraded, + Fixture::Stopping, + Fixture::FailedStart, + Fixture::NeedsAttention, + ] { + let mut m = model(f); + m.apply(Event::Select(fixtures::selected(f).into())); + if let Some(a) = fixtures::local(f).in_flight { + m.apply(Event::ActionStarted { + project: fixtures::selected(f).into(), + action: a, + }); + } + let d = m.detail().unwrap_or_else(|| panic!("{f:?}")); + assert_eq!(d.name, "storefront"); + assert_eq!(d.url.as_deref(), Some("https://storefront.hack")); + } + } +} diff --git a/apps/desktop/core/tests/dump_state.rs b/apps/desktop/core/tests/dump_state.rs new file mode 100644 index 000000000..79135841f --- /dev/null +++ b/apps/desktop/core/tests/dump_state.rs @@ -0,0 +1,199 @@ +//! Runs the real `hack-desktop-state` binary against a fake `hack` on PATH, +//! and a private fake daemon, and checks the JSON. +//! Hermetic: no real Hack home, daemon, Docker or network. + +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use std::process::Command; + +fn temp_dir(tag: &str) -> PathBuf { + // Short on macOS too: the daemon socket must fit in sockaddr_un. + let d = PathBuf::from("/tmp").join(format!( + "hack-desktop-dump-{tag}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + use std::os::unix::fs::DirBuilderExt; + std::fs::DirBuilder::new().mode(0o700).create(&d).unwrap(); + d +} + +fn serve_snapshot(home: &std::path::Path) -> std::thread::JoinHandle<()> { + use std::io::{Read, Write}; + use std::os::unix::{fs::DirBuilderExt, net::UnixListener}; + let dir = home.join("daemon"); + std::fs::DirBuilder::new().mode(0o700).create(&dir).unwrap(); + let listener = UnixListener::bind(dir.join("hackd.sock")).unwrap(); + listener.set_nonblocking(true).unwrap(); + std::thread::spawn(move || { + let bodies = [ + r#"{"status":"ok","version":"5.0.0"}"#, + r#"{"runtime_ok":true,"projects":[{"name":"alpha","status":"running","repo_root":"/work/alpha","defined_services":["api"],"runtime":{"services":[{"service":"api","containers":[{"state":"running","status":"Up 5 minutes"}]}]}},{"name":"beta","status":"stopped","repo_root":"/work/beta","defined_services":["web"]}]}"#, + ]; + for body in bodies { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); + let (mut stream, _) = loop { + match listener.accept() { + Ok(pair) => break pair, + Err(e) + if e.kind() == std::io::ErrorKind::WouldBlock + && std::time::Instant::now() < deadline => + { + std::thread::sleep(std::time::Duration::from_millis(5)) + } + Err(e) => panic!("fake daemon accept failed or timed out: {e}"), + } + }; + stream + .set_read_timeout(Some(std::time::Duration::from_secs(3))) + .unwrap(); + let mut request = [0; 4096]; + let n = stream.read(&mut request).unwrap(); + assert!(String::from_utf8_lossy(&request[..n]).starts_with("GET /v1/")); + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", + body.len(), + body + ) + .unwrap(); + } + }) +} + +#[test] +fn dump_state_reads_the_cli_like_the_app_and_prints_view_models() { + let bin = temp_dir("bin"); + let home = temp_dir("home"); + let hack = bin.join("hack"); + std::fs::write( + &hack, + r#"#!/bin/sh +case "$1" in + --version) echo "hack v5.0.0" ;; + projects) echo called > "$HACK_HOME/unsafe-listing"; exit 2 ;; + *) echo "unexpected: $*" >&2; exit 2 ;; +esac +"#, + ) + .unwrap(); + std::fs::set_permissions(&hack, std::fs::Permissions::from_mode(0o755)).unwrap(); + // Keep macOS first-launch scanning outside the fake daemon's accept + // deadline, using the same bounded runner as the application. + hack_desktop_core::runner::CliRunner::new(&hack) + .version(std::time::Duration::from_secs(60)) + .unwrap(); + let server = serve_snapshot(&home); + + let out = Command::new(env!("CARGO_BIN_EXE_hack-desktop-state")) + .env("PATH", format!("{}:/usr/bin:/bin", bin.display())) + .env("HACK_HOME", &home) + .env("HOME", &home) + .env("HACK_DAEMON_AUTOSTART_DISABLE", "1") + .output() + .unwrap(); + server.join().unwrap(); + assert!( + !home.join("unsafe-listing").exists(), + "no registry-writing fallback" + ); + let _ = std::fs::remove_dir_all(&bin); + let _ = std::fs::remove_dir_all(&home); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + + assert_eq!(v["source"]["ok"], true); + assert_eq!(v["source"]["cli_version"], "5.0.0"); + assert_eq!(v["source"]["daemon"]["version"], "5.0.0"); + assert_eq!(v["list"]["phase"]["phase"], "ready"); + assert_eq!(v["list"]["banners"], serde_json::json!([])); + assert_eq!(v["list"]["running"][0]["name"], "alpha"); + assert_eq!(v["list"]["stopped"][0]["name"], "beta"); + let details = v["details"].as_array().unwrap(); + assert_eq!(details.len(), 2); + assert_eq!(details[0]["name"], "alpha"); + assert_eq!(details[0]["state"]["state"], "running"); + assert_eq!(details[0]["services"][0]["name"], "api"); + assert_eq!(details[1]["state"]["state"], "stopped"); +} + +#[test] +fn dump_state_without_hack_reports_the_missing_cli() { + let empty = temp_dir("empty"); + let out = Command::new(env!("CARGO_BIN_EXE_hack-desktop-state")) + .env("PATH", empty.display().to_string()) + .env("HACK_HOME", &empty) + .env("HOME", &empty) + .env("HACK_DAEMON_AUTOSTART_DISABLE", "1") + .output() + .unwrap(); + let _ = std::fs::remove_dir_all(&empty); + assert!(out.status.success()); + let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + assert_eq!(v["source"]["ok"], false); + assert_eq!(v["list"]["phase"]["phase"], "cli_missing"); +} + +#[test] +fn missing_daemon_never_runs_the_registry_writing_cli_listing() { + let home = temp_dir("observe"); + let hack = home.join("hack"); + std::fs::write(&hack, "#!/bin/sh\ncase \"$1\" in --version) echo 'hack v5.0.0';; *) echo called > \"$HACK_HOME/unexpected\"; exit 1;; esac\n").unwrap(); + std::fs::set_permissions(&hack, std::fs::Permissions::from_mode(0o700)).unwrap(); + let out = Command::new(env!("CARGO_BIN_EXE_hack-desktop-state")) + .env("PATH", &home) + .env("HOME", &home) + .env("HACK_HOME", &home) + .env("HACK_DAEMON_AUTOSTART_DISABLE", "1") + .output() + .unwrap(); + assert!(out.status.success()); + assert!(!home.join("unexpected").exists()); + let body: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + assert_eq!(body["source"]["ok"], false); + assert!( + body["source"]["error"] + .as_str() + .unwrap() + .contains("hackd is off") + ); + assert_eq!(body["list"]["phase"]["phase"], "error"); + std::fs::remove_dir_all(home).unwrap(); +} + +#[test] +fn daemon_recovery_runs_only_the_requested_command_and_checks_its_exit() { + use hack_desktop_core::runner::CliRunner; + use hack_desktop_core::source::{DaemonRecovery, LiveSource, Source}; + let home = temp_dir("recover"); + let hack = home.join("hack"); + std::fs::write(&hack, "#!/bin/sh\nprintf '%s' \"$*\" > \"$HACK_HOME/called\"\nif [ \"$2\" = restart ]; then echo 'daemon could not restart' >&2; exit 1; fi\n").unwrap(); + std::fs::set_permissions(&hack, std::fs::Permissions::from_mode(0o700)).unwrap(); + let source = LiveSource { + runner: CliRunner::new(hack) + .with_env("HOME", home.to_str().unwrap()) + .with_env("HACK_HOME", home.to_str().unwrap()) + .with_env("HACK_DAEMON_AUTOSTART_DISABLE", "1"), + daemon: None, + }; + assert!(!home.join("called").exists()); + source.recover_daemon(DaemonRecovery::Start).unwrap(); + assert_eq!( + std::fs::read_to_string(home.join("called")).unwrap(), + "daemon start" + ); + let error = source.recover_daemon(DaemonRecovery::Restart).unwrap_err(); + assert!(error.contains("daemon could not restart")); + assert_eq!( + std::fs::read_to_string(home.join("called")).unwrap(), + "daemon restart" + ); + std::fs::remove_dir_all(home).unwrap(); +} diff --git a/apps/desktop/mise.toml b/apps/desktop/mise.toml new file mode 100644 index 000000000..55f6bde73 --- /dev/null +++ b/apps/desktop/mise.toml @@ -0,0 +1,4 @@ +# The desktop workspace needs Rust 1.98.1 (see rust-toolchain.toml); the repo +# root keeps its own pin. +[tools] +rust = "1.98.1" diff --git a/apps/desktop/rust-toolchain.toml b/apps/desktop/rust-toolchain.toml new file mode 100644 index 000000000..0a144be41 --- /dev/null +++ b/apps/desktop/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# Pinned for this workspace only (macOS needs >= 1.98: gpui-pre-macos 0.3.12 +# uses Option::map_or_default). The repo root pin is unchanged. +channel = "1.98.1" +components = ["rustfmt", "clippy"] diff --git a/apps/desktop/screenshots/fixtures-dark.png b/apps/desktop/screenshots/fixtures-dark.png new file mode 100644 index 000000000..51ce1fe01 Binary files /dev/null and b/apps/desktop/screenshots/fixtures-dark.png differ diff --git a/apps/desktop/screenshots/fixtures-light.png b/apps/desktop/screenshots/fixtures-light.png new file mode 100644 index 000000000..68520107b Binary files /dev/null and b/apps/desktop/screenshots/fixtures-light.png differ