diff --git a/.cargo/audit.toml b/.cargo/audit.toml index b6dd47060380..0e271c7fbbca 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -8,4 +8,17 @@ ignore = [ "RUSTSEC-2024-0370", # time crate can't be updated in the repo because of MSRV, users are unaffected "RUSTSEC-2026-0009", + # rand unsoundness, fixed when we remove kuchikiki from deps in v3, currently + # remains for semver reasons but is not built in default configuration + "RUSTSEC-2026-0097", + # glib 0.18.5 unsoundness, fixed by updating to gtk4 + "RUSTSEC-2024-0429", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0049", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0098", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0099", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0104", ] diff --git a/.changes/base64.md b/.changes/base64.md deleted file mode 100644 index 2fb599baba49..000000000000 --- a/.changes/base64.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"tauri-macos-sign": patch:enhance ---- - -Do not rely on system base64 CLI to decode certificates. diff --git a/.changes/bundle-vc-runtime.md b/.changes/bundle-vc-runtime.md new file mode 100644 index 000000000000..15d7fa313c29 --- /dev/null +++ b/.changes/bundle-vc-runtime.md @@ -0,0 +1,7 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"tauri-utils": "minor:feat" +--- + +Added `bundle.windows.bundleVCRuntime` to copy the Visual C++ runtime DLLs into Windows MSI and NSIS installers. The bundler locates the runtime through `VCTOOLS_REDIST_DIR` or the bundled `vswhere.exe`. diff --git a/.changes/data-tauri-drag-region-deep.md b/.changes/data-tauri-drag-region-deep.md deleted file mode 100644 index 1391ff5ac589..000000000000 --- a/.changes/data-tauri-drag-region-deep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"tauri": minor:feat ---- - -Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"` diff --git a/.changes/prompt-signing-key-password-context.md b/.changes/prompt-signing-key-password-context.md deleted file mode 100644 index 59f9fbb9bf17..000000000000 --- a/.changes/prompt-signing-key-password-context.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"tauri-cli": patch:enhance -"@tauri-apps/cli": patch:enhance ---- - -Show the context before prompting for updater signing key password diff --git a/.changes/static-vc-runtime.md b/.changes/static-vc-runtime.md new file mode 100644 index 000000000000..22f770e27167 --- /dev/null +++ b/.changes/static-vc-runtime.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": "minor:feat" +"tauri-utils": "minor:feat" +--- + +Added `build.windows.staticVCRuntime` to control MSVC static runtime linking. The `STATIC_VCRUNTIME` environment variable is now deprecated and emits a migration warning when used. diff --git a/.changes/tauri-build-static-vc-runtime.md b/.changes/tauri-build-static-vc-runtime.md new file mode 100644 index 000000000000..98f0d0517137 --- /dev/null +++ b/.changes/tauri-build-static-vc-runtime.md @@ -0,0 +1,5 @@ +--- +"tauri-build": "minor:feat" +--- + +Added `tauri_build::WindowsAttributes::static_vc_runtime` to control MSVC static runtime linking from build scripts. diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index d840272be8bd..157717061338 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,6 +16,8 @@ on: - '**/package.json' - '**/pnpm-lock.yaml' push: + branches: + - dev paths: - '.github/workflows/audit.yml' - '**/Cargo.lock' diff --git a/.github/workflows/lint-rust.yml b/.github/workflows/lint-rust.yml index b71cc1648eca..8b2ca12d90c7 100644 --- a/.github/workflows/lint-rust.yml +++ b/.github/workflows/lint-rust.yml @@ -12,6 +12,8 @@ on: paths: - '.github/workflows/lint-rust.yml' - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' env: RUST_BACKTRACE: 1 diff --git a/.github/workflows/publish-cli-js.yml b/.github/workflows/publish-cli-js.yml index a2a53afab4e0..4cfa965fb18e 100644 --- a/.github/workflows/publish-cli-js.yml +++ b/.github/workflows/publish-cli-js.yml @@ -53,6 +53,8 @@ jobs: docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | npm i -g --force corepack + rustup install 1.88.0 + rustup default 1.88.0 cd packages/cli pnpm build --target x86_64-unknown-linux-gnu strip *.node @@ -60,12 +62,15 @@ jobs: target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | + rustup install 1.88.0 + rustup default 1.88.0 cd packages/cli pnpm build strip *.node - host: macos-latest target: aarch64-apple-darwin build: | + rustup target add x86_64-apple-darwin pnpm build --features native-tls-vendored --target=aarch64-apple-darwin strip -x *.node - host: ubuntu-22.04 @@ -73,6 +78,9 @@ jobs: docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | npm i -g --force corepack + rustup install 1.88.0 + rustup default 1.88.0 + rustup target add aarch64-unknown-linux-gnu cd packages/cli pnpm build --target aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu-strip *.node @@ -90,8 +98,10 @@ jobs: target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | - cd packages/cli + rustup install 1.88.0 + rustup default 1.88.0 rustup target add aarch64-unknown-linux-musl + cd packages/cli pnpm build --target aarch64-unknown-linux-musl /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node - host: ubuntu-22.04 @@ -107,6 +117,8 @@ jobs: runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 + - name: Prepare CEF package metadata and versions + run: node ../../.scripts/ci/prepare-cli-cef-publish.js - run: npm i -g --force corepack - name: Setup node uses: actions/setup-node@v4 @@ -136,7 +148,12 @@ jobs: if: ${{ matrix.settings.docker }} with: image: ${{ matrix.settings.docker }} - options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/root/.cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/root/.cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/root/.cargo/registry/index -v ${{ github.workspace }}:/build -w /build + options: --user 0:0 -v ${{ github.workspace + }}/.cargo-cache/git/db:/root/.cargo/git/db -v ${{ github.workspace + }}/.cargo/registry/cache:/root/.cargo/registry/cache -v ${{ + github.workspace + }}/.cargo/registry/index:/root/.cargo/registry/index -v ${{ + github.workspace }}:/build -w /build run: ${{ matrix.settings.build }} - name: Build @@ -345,7 +362,8 @@ jobs: - uses: addnab/docker-run-action@v3 with: image: ${{ matrix.image }} - options: '-v ${{ github.workspace }}:/build -w /build -e RUSTUP_HOME=/usr/local/rustup -e CARGO_HOME=/usr/local/cargo' + options: '-v ${{ github.workspace }}:/build -w /build -e + RUSTUP_HOME=/usr/local/rustup -e CARGO_HOME=/usr/local/cargo' shell: bash run: | set -e @@ -386,12 +404,14 @@ jobs: path: packages/cli/artifacts - name: Move artifacts run: pnpm artifacts + - name: Rewrite package names for CEF publish + run: node ../../.scripts/ci/prepare-cli-cef-publish.js - name: List packages run: ls -R ./npm shell: bash - name: Publish run: | - npm publish + npm publish --tag latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: '' diff --git a/.github/workflows/publish-cli-rs.yml b/.github/workflows/publish-cli-rs.yml index 15698e932fe3..fa02c2adbba2 100644 --- a/.github/workflows/publish-cli-rs.yml +++ b/.github/workflows/publish-cli-rs.yml @@ -76,22 +76,21 @@ jobs: - name: Build CLI if: ${{ !matrix.config.cross }} - run: cargo build --manifest-path ./crates/tauri-cli/Cargo.toml --profile release-size-optimized ${{ matrix.config.args }} + run: >- + cargo build --manifest-path ./crates/tauri-cli/Cargo.toml + --target ${{ matrix.config.rust_target }} + --profile release-size-optimized + ${{ matrix.config.args }} - name: Build CLI (cross) if: ${{ matrix.config.cross }} - run: cross build --manifest-path ./crates/tauri-cli/Cargo.toml --target ${{ matrix.config.rust_target }} --profile release-size-optimized ${{ matrix.config.args }} + run: >- + cross build --manifest-path ./crates/tauri-cli/Cargo.toml + --target ${{ matrix.config.rust_target }} + --profile release-size-optimized + ${{ matrix.config.args }} - name: Upload CLI - if: ${{ !matrix.config.cross }} - uses: actions/upload-artifact@v4 - with: - name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }} - path: target/release-size-optimized/cargo-tauri${{ matrix.config.ext }} - if-no-files-found: error - - - name: Upload CLI (cross) - if: ${{ matrix.config.cross }} uses: actions/upload-artifact@v4 with: name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }} diff --git a/.github/workflows/test-cli-rs.yml b/.github/workflows/test-cli-rs.yml index f7a5a08ca355..15f7d7de6bc2 100644 --- a/.github/workflows/test-cli-rs.yml +++ b/.github/workflows/test-cli-rs.yml @@ -14,6 +14,8 @@ on: - 'crates/tauri-utils/**' - 'crates/tauri-bundler/**' - 'crates/tauri-cli/**' + - 'Cargo.toml' + - 'Cargo.lock' env: RUST_BACKTRACE: 1 diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index 0256448e0e58..5c228bb11f5b 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -12,6 +12,8 @@ on: paths: - '.github/workflows/test-core.yml' - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' - '!crates/tauri/scripts/**' - '!crates/tauri-cli/**' - '!crates/tauri-bundler/**' diff --git a/.scripts/ci/prepare-cli-cef-publish.js b/.scripts/ci/prepare-cli-cef-publish.js new file mode 100755 index 000000000000..daf16c142304 --- /dev/null +++ b/.scripts/ci/prepare-cli-cef-publish.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const { readdirSync, readFileSync, writeFileSync } = require('node:fs') +const { join } = require('node:path') + +const SOURCE_NAME = '@tauri-apps/cli' +const TARGET_NAME = '@tauri-apps/cli-cef' + +const cliDir = process.cwd() +const npmDir = join(cliDir, 'npm') +const cefCliVersionPath = join(cliDir, '.cef-cli-version') +const tauriCliCargoTomlPath = join(cliDir, '../../crates/tauri-cli/Cargo.toml') + +const cefCliVersion = readFileSync(cefCliVersionPath, 'utf8').trim() + +if (!cefCliVersion) { + throw new Error(`expected a version in ${cefCliVersionPath}`) +} + +function rewritePackageName(packageJsonPath, setVersion = false) { + const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) + if (setVersion) { + pkg.version = cefCliVersion + } + if (typeof pkg.name === 'string' && pkg.name.startsWith(SOURCE_NAME)) { + pkg.name = pkg.name.replace(SOURCE_NAME, TARGET_NAME) + } + writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`) + console.log(`updated package metadata in ${packageJsonPath}`) +} + +rewritePackageName(join(cliDir, 'package.json'), true) + +for (const entry of readdirSync(npmDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue + } + rewritePackageName(join(npmDir, entry.name, 'package.json')) +} + +const indexJsPath = join(cliDir, 'index.js') +const indexContents = readFileSync(indexJsPath, 'utf8') +const rewrittenIndexContents = indexContents.replace( + /@tauri-apps\/cli(?=[-/'"`])/g, + TARGET_NAME +) + +if (rewrittenIndexContents !== indexContents) { + writeFileSync(indexJsPath, rewrittenIndexContents) + console.log(`rewrote native binding imports in ${indexJsPath}`) +} + +const tauriCliCargoToml = readFileSync(tauriCliCargoTomlPath, 'utf8') +const tauriCliCargoTomlWithVersion = tauriCliCargoToml.replace( + /^version = ".*"$/m, + `version = "${cefCliVersion}"` +) + +if (tauriCliCargoTomlWithVersion !== tauriCliCargoToml) { + writeFileSync(tauriCliCargoTomlPath, tauriCliCargoTomlWithVersion) + console.log(`updated tauri-cli version in ${tauriCliCargoTomlPath}`) +} diff --git a/Cargo.lock b/Cargo.lock index 171e8afe45b6..7f78e8c835f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,7 +292,7 @@ dependencies = [ "pkcs1", "pkcs8", "plist", - "rand 0.8.5", + "rand 0.8.6", "rasn", "rayon", "regex", @@ -359,7 +359,7 @@ dependencies = [ "flate2", "log", "md-5", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.11.27", "scroll", "serde", @@ -440,7 +440,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror 1.0.69", @@ -539,7 +539,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -774,7 +774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0" dependencies = [ "bitvec", - "nom", + "nom 7.1.3", ] [[package]] @@ -815,11 +815,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1064,9 +1064,9 @@ dependencies = [ [[package]] name = "cargo-mobile2" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcea7efeaac9f0fd9f886f43a13dde186a1e2266fe6b53a42659e4e0689570de" +checksum = "caa0060b6b7e1c0f14312c8ccebf28f5713b3045ffaae033a180622122a361dc" dependencies = [ "colored", "core-foundation 0.10.0", @@ -1083,16 +1083,17 @@ dependencies = [ "java-properties", "libc", "log", - "once-cell-regex", + "once_cell", "os_info", "os_pipe", "path_abs", "plist", + "regex", "serde", "serde_json", "textwrap", "thiserror 2.0.12", - "toml 0.9.10+spec-1.1.0", + "toml 1.0.6+spec-1.1.0", "ureq", "which", "windows 0.61.1", @@ -1129,7 +1130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -1173,24 +1174,25 @@ dependencies = [ [[package]] name = "cef" -version = "146.4.1+146.0.9" +version = "148.0.0+147.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5dad6495c583fedab04a24f6fc08274c59a28b33967ca87709398e1f11c2ebe" +checksum = "2445d2f1e820efc064c511faa3a07c3ee79e565fdae026ca62ed3c80d0459223" dependencies = [ "cef-dll-sys", "libloading 0.9.0", - "objc2 0.6.3", + "objc2 0.6.4", "windows-sys 0.61.1", ] [[package]] name = "cef-dll-sys" -version = "146.4.1+146.0.9" -source = "git+https://github.com/tauri-apps/cef-rs?branch=fix%2F146-location-windows#7fc79c7aa4f1d691ed9996d440f13eb34a151f2d" +version = "148.0.0+147.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d32a7c89af6c77688ad58dc75555c9398c5ded2c328a3ff83554fccc840af57" dependencies = [ "anyhow", "cmake", - "download-cef 2.3.1 (git+https://github.com/tauri-apps/cef-rs?branch=fix%2F146-location-windows)", + "download-cef", "serde_json", ] @@ -1361,7 +1363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -1534,6 +1536,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.7.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1705,6 +1720,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -1717,29 +1745,29 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor 0.1.1", ] [[package]] name = "ctor" -version = "0.4.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ "ctor-proc-macro", - "dtor", + "dtor 0.3.0", ] [[package]] name = "ctor-proc-macro" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" [[package]] name = "ctr" @@ -1858,6 +1886,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + [[package]] name = "der" version = "0.7.9" @@ -1940,7 +1979,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -1955,6 +2003,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "des" version = "0.8.1" @@ -2056,10 +2115,16 @@ dependencies = [ ] [[package]] -name = "dispatch" -version = "0.2.0" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] [[package]] name = "displaydoc" @@ -2103,36 +2168,33 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] -name = "download-cef" -version = "2.3.1" +name = "dom_query" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7471a7d5d3bd8df1b3b75871f0317d8a6dd73270ab861cc97ae7907ee63e554" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ - "bzip2 0.6.0", - "clap", - "indicatif", - "regex", - "semver", - "serde", - "serde_json", - "sha1_smol", - "tar", - "thiserror 2.0.12", - "ureq", + "bit-set", + "cssparser 0.36.0", + "foldhash", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", ] [[package]] name = "download-cef" version = "2.3.1" -source = "git+https://github.com/tauri-apps/cef-rs?branch=fix%2F146-location-windows#7fc79c7aa4f1d691ed9996d440f13eb34a151f2d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7471a7d5d3bd8df1b3b75871f0317d8a6dd73270ab861cc97ae7907ee63e554" dependencies = [ "bzip2 0.6.0", "clap", @@ -2189,24 +2251,33 @@ dependencies = [ [[package]] name = "dtor" -version = "0.0.6" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" dependencies = [ "dtor-proc-macro", ] [[package]] name = "dtor-proc-macro" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" [[package]] name = "duct" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ce170a0e8454fa0f9b0e5ca38a6ba17ed76a50916839d217eb5357e05cdfde" +checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684" dependencies = [ "libc", "os_pipe", @@ -2339,7 +2410,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg 0.55.0", ] @@ -2629,6 +2700,12 @@ 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 = "fontconfig-parser" version = "0.5.7" @@ -2715,12 +2792,12 @@ dependencies = [ [[package]] name = "freedesktop_entry_parser" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" +checksum = "2b368437186ec63ceb50d0832ee1ebcb5878037fe16ead1c68081d4aee0d140a" dependencies = [ - "nom", - "thiserror 1.0.69", + "indexmap 2.11.4", + "nom 8.0.0", ] [[package]] @@ -3369,10 +3446,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "0.2.12" @@ -3601,7 +3688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -3787,7 +3874,7 @@ dependencies = [ "gif", "image-webp", "num-traits", - "png", + "png 0.17.16", "qoi", "ravif", "rayon", @@ -4107,10 +4194,12 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -4195,7 +4284,7 @@ dependencies = [ "jsonrpsee-types", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "rustc-hash", "serde", "serde_json", @@ -4369,10 +4458,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.11.4", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -4436,6 +4525,15 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.8" @@ -4458,12 +4556,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4473,7 +4571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -4553,9 +4651,9 @@ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "local-ip-address" @@ -4642,9 +4740,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.7", + "string_cache_codegen 0.5.2", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -4793,34 +4902,34 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", "libxdo", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.0", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.12", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] name = "napi" -version = "3.0.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf3f418eeb94d86f02f3388324017e1e21e36937e4b1d8075ea5843d980f766" +checksum = "c3a1135cfe16ca43ac82ac05858554fc39c037d8e4592f2b4a83d7ef8e822f43" dependencies = [ "bitflags 2.7.0", - "ctor 0.4.2", + "ctor 0.6.3", "napi-build", "napi-sys", "nohash-hasher", @@ -4829,18 +4938,18 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.2.2" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff539e61c5e3dd4d7d283610662f5d672c2aea0f158df78af694f13dbb3287b" +checksum = "3ae82775d1b06f3f07efd0666e59bbc175da8383bc372051031d7a447e94fbea" [[package]] name = "napi-derive" -version = "3.0.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce09c2a155c7c38447a6ff1711899375bdd8f9dd5a50aad700a9c765c9f8a375" +checksum = "78665d6bdf10e9a4e6b38123efb0f66962e6197c1aea2f07cff3f159a374696d" dependencies = [ "convert_case 0.8.0", - "ctor 0.4.2", + "ctor 0.6.3", "napi-derive-backend", "proc-macro2", "quote", @@ -4849,9 +4958,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17923387d68ecfe057a04a38ee89aeb41f415c43b4c7a508a3683bf0c1511c5a" +checksum = "42d55d01423e7264de3acc13b258fa48ca7cf38a4d25db848908ec3c1304a85a" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -4862,11 +4971,11 @@ dependencies = [ [[package]] name = "napi-sys" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" +checksum = "1ed8f0e23a62a3ce0fbb6527cdc056e9282ddd9916b068c46f8923e18eed5ee6" dependencies = [ - "libloading 0.8.6", + "libloading 0.8.9", ] [[package]] @@ -4901,12 +5010,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -4981,6 +5084,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonmax" version = "0.5.5" @@ -5065,7 +5177,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", "smallvec", "zeroize", @@ -5207,9 +5319,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -5222,9 +5334,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", + "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", @@ -5241,7 +5353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.0", ] @@ -5252,7 +5364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.0", ] @@ -5263,7 +5375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -5273,7 +5385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -5284,7 +5396,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.0", ] @@ -5322,9 +5444,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", + "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -5335,7 +5457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -5371,7 +5493,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", + "objc2-core-foundation", "objc2-foundation 0.3.0", ] @@ -5382,7 +5505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3126341c65c5d5728423ae95d788e1b660756486ad0592307ab87ba02d9a7268" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -5393,8 +5516,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.3.0", + "objc2-quartz-core 0.3.0", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.0", ] @@ -5405,8 +5546,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.0", @@ -5457,21 +5598,11 @@ dependencies = [ "asn1-rs", ] -[[package]] -name = "once-cell-regex" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de7e389a5043420c8f2b95ed03f3f104ad6f4c41f7d7e27298f033abc253e8" -dependencies = [ - "once_cell", - "regex", -] - [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" @@ -5481,9 +5612,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.7.0", "cfg-if", @@ -5528,9 +5659,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -5567,12 +5698,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -6022,7 +6153,7 @@ dependencies = [ "k256", "log", "md-5", - "nom", + "nom 7.1.3", "num-bigint-dig", "num-traits", "num_enum", @@ -6030,7 +6161,7 @@ dependencies = [ "p256", "p384", "p521", - "rand 0.8.5", + "rand 0.8.6", "ripemd", "rsa", "sha1", @@ -6076,6 +6207,17 @@ 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 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -6096,6 +6238,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -6113,7 +6265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -6123,7 +6275,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", +] + +[[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]] @@ -6153,6 +6315,19 @@ dependencies = [ "syn 2.0.117", ] +[[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.117", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6180,6 +6355,15 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -6277,6 +6461,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.7.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polyval" version = "0.6.2" @@ -6395,9 +6592,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -6445,7 +6642,7 @@ dependencies = [ "bitflags 2.7.0", "lazy_static", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", @@ -6512,7 +6709,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger 0.8.4", "log", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -6563,9 +6760,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -6574,9 +6771,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -6680,7 +6877,7 @@ dependencies = [ "either", "jzon", "konst", - "nom", + "nom 7.1.3", "num-bigint", "num-integer", "num-traits", @@ -6730,7 +6927,7 @@ dependencies = [ "once_cell", "paste", "profiling", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "simd_helpers", "system-deps", @@ -6936,9 +7133,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -7123,7 +7320,7 @@ dependencies = [ "itertools 0.13.0", "log", "md-5", - "nom", + "nom 7.1.3", "num", "num-derive", "num-traits", @@ -7175,7 +7372,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_json", @@ -7208,7 +7405,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -7287,7 +7484,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -7370,7 +7567,7 @@ dependencies = [ "rustls 0.23.35", "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.13", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", @@ -7406,9 +7603,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -7488,9 +7685,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e1c91382686d21b5ac7959341fcb9780fa7c03773646995a87c950fa7be640" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] @@ -7580,9 +7777,9 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.5" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" [[package]] name = "seahash" @@ -7647,14 +7844,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", + "cssparser 0.29.6", "derive_more 0.99.18", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.7.0", + "cssparser 0.36.0", + "derive_more 2.0.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -7933,6 +8149,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7984,19 +8209,20 @@ dependencies = [ [[package]] name = "shared_child" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" dependencies = [ "libc", - "windows-sys 0.59.0", + "sigchld", + "windows-sys 0.60.2", ] [[package]] name = "shared_thread" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a6f98357c6bb0ebace19b22220e5543801d9de90ffe77f8abb27c056bac064" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" [[package]] name = "shell-words" @@ -8010,6 +8236,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook 0.3.18", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook" version = "0.4.1" @@ -8037,7 +8284,7 @@ checksum = "e513e435a8898a0002270f29d0a708b7879708fb5c4d00e46983ca2d2d378cf0" dependencies = [ "futures-core", "libc", - "signal-hook", + "signal-hook 0.4.1", "tokio", ] @@ -8197,7 +8444,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "js-sys", "log", @@ -8223,7 +8470,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "sha1", ] @@ -8351,6 +8598,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.2" @@ -8363,12 +8622,45 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sublime_fuzzy" version = "0.7.0" @@ -8589,35 +8881,35 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", + "block2 0.6.2", "core-foundation 0.10.0", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", - "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.0", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -8646,9 +8938,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -8663,7 +8955,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.1" dependencies = [ "anyhow", "bytes", @@ -8685,7 +8977,7 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.0", "objc2-ui-kit", @@ -8696,7 +8988,7 @@ dependencies = [ "quickcheck", "quickcheck_macros", "raw-window-handle", - "reqwest 0.13.1", + "reqwest 0.13.3", "rustls 0.23.35", "serde", "serde_json", @@ -8725,7 +9017,7 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.1" dependencies = [ "anyhow", "cargo_toml", @@ -8741,13 +9033,12 @@ dependencies = [ "tauri-codegen", "tauri-utils", "tauri-winres", - "toml 0.9.10+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-bundler" -version = "2.8.1" +version = "2.9.1" dependencies = [ "anyhow", "ar", @@ -8793,7 +9084,7 @@ dependencies = [ [[package]] name = "tauri-cli" -version = "2.10.1" +version = "2.11.1" dependencies = [ "ar", "axum", @@ -8807,7 +9098,7 @@ dependencies = [ "ctrlc", "dialoguer", "dirs 6.0.0", - "download-cef 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "download-cef", "duct", "dunce", "elf", @@ -8815,7 +9106,6 @@ dependencies = [ "glob", "handlebars", "heck 0.5.0", - "html5ever", "ignore", "image", "include_dir", @@ -8828,7 +9118,6 @@ dependencies = [ "jsonrpsee-core", "jsonrpsee-ws-client", "jsonschema", - "kuchikiki", "libc", "local-ip-address", "log", @@ -8844,10 +9133,10 @@ dependencies = [ "oxc_ast", "oxc_parser", "oxc_span", - "phf 0.11.3", + "phf 0.13.1", "plist", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.4", "rayon", "regex", "resvg", @@ -8864,8 +9153,8 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "tokio", - "toml 0.9.10+spec-1.1.0", - "toml_edit 0.24.0+spec-1.1.0", + "toml 1.0.6+spec-1.1.0", + "toml_edit 0.25.4+spec-1.1.0", "ureq", "url", "uuid", @@ -8883,19 +9172,21 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "napi-derive-backend", + "napi-sys", "tauri-cli", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.1" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -8913,7 +9204,7 @@ dependencies = [ [[package]] name = "tauri-driver" -version = "2.0.5" +version = "2.0.6" dependencies = [ "anyhow", "futures", @@ -8924,7 +9215,7 @@ dependencies = [ "pico-args", "serde", "serde_json", - "signal-hook", + "signal-hook 0.4.1", "signal-hook-tokio", "tokio", "which", @@ -8949,12 +9240,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03b7eb4d0d43724ba9ba6a6717420ee68aee377816a3edbb45db8c18862b1431" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "tauri-macos-sign" -version = "2.3.3" +version = "2.3.4" dependencies = [ "apple-codesign", "base64 0.22.1", @@ -8965,7 +9256,7 @@ dependencies = [ "os_pipe", "p12", "plist", - "rand 0.9.1", + "rand 0.9.4", "regex", "serde", "serde_json", @@ -8976,7 +9267,7 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.1" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -8988,7 +9279,7 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.1" dependencies = [ "anyhow", "glob", @@ -8997,7 +9288,6 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.10+spec-1.1.0", "walkdir", ] @@ -9010,7 +9300,7 @@ dependencies = [ "byte-unit", "fern", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.0", "serde", "serde_json", @@ -9035,14 +9325,14 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.1" dependencies = [ "cookie", "dpi", "gtk", "http 1.3.1", "jni", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-ui-kit", "raw-window-handle", "serde", @@ -9063,10 +9353,11 @@ dependencies = [ "dioxus-debug-cell", "dirs 6.0.0", "gtk", - "html5ever", + "html5ever 0.29.1", "http 1.3.1", "kuchikiki", - "objc2 0.6.3", + "log", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.0", "raw-window-handle", @@ -9082,13 +9373,13 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.1" dependencies = [ "gtk", "http 1.3.1", "jni", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-web-kit", "once_cell", @@ -9133,17 +9424,18 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.1" dependencies = [ "aes-gcm", "anyhow", "brotli", "cargo_metadata", - "ctor 0.2.9", + "ctor 0.8.0", + "dom_query", "dunce", "getrandom 0.3.3", "glob", - "html5ever", + "html5ever 0.29.1", "http 1.3.1", "infer", "json-patch", @@ -9151,7 +9443,8 @@ dependencies = [ "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex", @@ -9165,8 +9458,9 @@ dependencies = [ "serialize-to-javascript", "swift-rs", "tauri", + "tempfile", "thiserror 2.0.12", - "toml 0.9.10+spec-1.1.0", + "toml 1.0.6+spec-1.1.0", "url", "urlpattern", "uuid", @@ -9181,7 +9475,7 @@ checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9" dependencies = [ "embed-resource", "indexmap 2.11.4", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -9220,6 +9514,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -9346,7 +9650,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -9510,9 +9814,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.11.4", "serde_core", @@ -9523,6 +9827,21 @@ dependencies = [ "winnow 0.7.14", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -9541,6 +9860,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -9578,14 +9906,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.24.0+spec-1.1.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.11.4", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -9593,9 +9921,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow 0.7.14", ] @@ -9701,24 +10029,24 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" +checksum = "1064cf4fbfa2c60f453e7a9125bc53add41de2d83d1cba7a497636e6520c345b" dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.0", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -9748,7 +10076,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "rustls 0.22.4", "rustls-native-certs 0.7.3", "rustls-pki-types", @@ -9769,7 +10097,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.9.1", + "rand 0.9.4", "sha1", "thiserror 2.0.12", "utf-8", @@ -9802,9 +10130,9 @@ checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "typewit" @@ -10286,9 +10614,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -10299,22 +10627,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10322,9 +10647,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -10335,18 +10660,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -10357,9 +10682,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -10375,6 +10700,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -10534,7 +10871,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -10549,7 +10886,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.0", @@ -10652,9 +10989,9 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -10755,7 +11092,7 @@ version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -11064,9 +11401,9 @@ dependencies = [ [[package]] name = "worker" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42c76c5889873a2c309365ad4503810c007d3c25fbb4e9fa9e4e23c4ceb3c7f2" +checksum = "4afd7ae4f7fcc11e0e5e64b964890b3dda90f1290b0612f7cd821b381cc18826" dependencies = [ "async-trait", "axum", @@ -11095,13 +11432,14 @@ dependencies = [ [[package]] name = "worker-macros" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c62584d037bad33789a6a5d605b3fccea1c52de9251d06f9d44054170dc612" +checksum = "6371f41ac538c9f6dbe4d40cf7db58ed451eb0529a66f3e29ab8726217fc8a05" dependencies = [ "async-trait", "proc-macro2", "quote", + "strum", "syn 2.0.117", "wasm-bindgen", "wasm-bindgen-futures", @@ -11111,9 +11449,9 @@ dependencies = [ [[package]] name = "worker-sys" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ddd412fd62c6eeffc1dd85e6ae5960a33b534f44a733df75b6e7519972bc74" +checksum = "4c8de95c532944cee89d63fa8d7945f3db6260ca75ee3da42f7acfeebf538e4c" dependencies = [ "cfg-if", "js-sys", @@ -11135,27 +11473,26 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.54.0" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e456eeaf7f09413fdc16799782879b2b9f1d264dfdbce4cf7e924df0ef36afb9" +checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" dependencies = [ "base64 0.22.1", - "block2 0.6.0", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs 6.0.0", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http 1.3.1", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.0", diff --git a/Cargo.toml b/Cargo.toml index 744d5ed1cbb3..24751061df9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,4 +71,3 @@ tauri = { path = "./crates/tauri" } tauri-plugin = { path = "./crates/tauri-plugin" } tauri-utils = { path = "./crates/tauri-utils" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } -cef-dll-sys = { git = "https://github.com/tauri-apps/cef-rs", branch = "fix/146-location-windows" } diff --git a/cef-helper/Cargo.toml b/cef-helper/Cargo.toml index 45eac9bfafea..74ae3340292f 100644 --- a/cef-helper/Cargo.toml +++ b/cef-helper/Cargo.toml @@ -6,9 +6,9 @@ license = "Apache-2.0 OR MIT" publish = false [dependencies] -cef = { version = "=146.4.1", default-features = false } +cef = { version = "=148.0.0", default-features = false } # Not actually used directly, just locking it. -cef-dll-sys = { version = "=146.4.1", default-features = false } +cef-dll-sys = { version = "=148.0.0", default-features = false } [features] default = ["sandbox"] diff --git a/cef-helper/src/main.rs b/cef-helper/src/main.rs index 89002c24c431..aed6b8ebcacd 100644 --- a/cef-helper/src/main.rs +++ b/cef-helper/src/main.rs @@ -1,5 +1,122 @@ use cef::{args::Args, *}; +const IPC_MESSAGE_NAME: &str = "tauri:ipc"; +const IPC_POST_MESSAGE_FUNCTION: &str = "postMessage"; + +wrap_v8_handler! { + struct IpcPostMessageV8Handler; + + impl V8Handler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + retval: Option<&mut Option>, + exception: Option<&mut CefString>, + ) -> std::os::raw::c_int { + let Some(name) = name else { + return 0; + }; + if name.to_string() != IPC_POST_MESSAGE_FUNCTION { + return 0; + } + + let Some(message) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|argument| argument.is_string() != 0) + else { + if let Some(exception) = exception { + *exception = CefString::from("window.ipc.postMessage expects a string argument"); + } + return 1; + }; + + let Some(context) = v8_context_get_current_context() else { + return 1; + }; + let Some(frame) = context.frame() else { + return 1; + }; + + let body = CefString::from(&message.string_value()).to_string(); + let url = CefString::from(&frame.url()).to_string(); + let mut process_message = process_message_create(Some(&CefString::from(IPC_MESSAGE_NAME))); + if let Some(args) = process_message.as_ref().and_then(ProcessMessage::argument_list) { + args.set_string(0, Some(&CefString::from(url.as_str()))); + args.set_string(1, Some(&CefString::from(body.as_str()))); + frame.send_process_message(ProcessId::BROWSER, process_message.as_mut()); + } + + if let Some(retval) = retval { + *retval = v8_value_create_undefined(); + } + 1 + } + } +} + +fn install_ipc_post_message(context: Option<&mut V8Context>) { + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + + let Some(mut ipc) = v8_value_create_object(None, None) else { + return; + }; + let mut handler = IpcPostMessageV8Handler::new(); + let post_message_name = CefString::from(IPC_POST_MESSAGE_FUNCTION); + let Some(mut post_message) = + v8_value_create_function(Some(&post_message_name), Some(&mut handler)) + else { + return; + }; + + ipc.set_value_bykey( + Some(&post_message_name), + Some(&mut post_message), + attributes, + ); + window.set_value_bykey(Some(&CefString::from("ipc")), Some(&mut ipc), attributes); +} + +wrap_render_process_handler! { + struct TauriRenderProcessHandler; + + impl RenderProcessHandler { + fn on_context_created( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + install_ipc_post_message(context); + } + } +} + +wrap_app! { + struct TauriRenderApp; + + impl App { + fn render_process_handler(&self) -> Option { + Some(TauriRenderProcessHandler::new()) + } + } +} + fn main() { let args = Args::new(); @@ -17,11 +134,11 @@ fn main() { loader }; + let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); + let mut app = TauriRenderApp::new(); execute_process( Some(args.as_main_args()), - None::<&mut App>, + Some(&mut app), std::ptr::null_mut(), ); } - - diff --git a/crates/tauri-build/CHANGELOG.md b/crates/tauri-build/CHANGELOG.md index 121ffcf8668d..df654d6eda9f 100644 --- a/crates/tauri-build/CHANGELOG.md +++ b/crates/tauri-build/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-codegen@2.6.1` + +## \[2.6.0] + +### New Features + +- [`b7a0ff030`](https://www.github.com/tauri-apps/tauri/commit/b7a0ff03087a73fce6888361562faf9738afc829) ([#15263](https://www.github.com/tauri-apps/tauri/pull/15263)) Allow users to append extra `.rc` content by `append_rc_content` in `WindowsAttributes`. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. + +### Enhancements + +- [`d730770bb`](https://www.github.com/tauri-apps/tauri/commit/d730770bb93d77358cfc6f1286f10187cef37362) ([#15117](https://www.github.com/tauri-apps/tauri/pull/15117)) Simplify async-sync code boundaries, no externally visible changes +- [`b3f2d12b8`](https://www.github.com/tauri-apps/tauri/commit/b3f2d12b89daefe528e562b93871db62f77973b9) ([#15289](https://www.github.com/tauri-apps/tauri/pull/15289)) Preserve a numeric semver build identifier such as `1.2.3+42` in the 4th segment of the Windows `FILEVERSION` fixed field when it fits in the Windows version format. + +### Bug Fixes + +- [`a30dca482`](https://www.github.com/tauri-apps/tauri/commit/a30dca4820d0ad681f04737a1819e6a6fab9fe84) ([#15288](https://www.github.com/tauri-apps/tauri/pull/15288)) Set the correct Windows `FileVersion` and `ProductVersion` string values using the version from the Tauri config. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` +- Upgraded to `tauri-codegen@2.6.0` + ## \[2.5.6] ### Dependencies diff --git a/crates/tauri-build/Cargo.toml b/crates/tauri-build/Cargo.toml index c1c99bc32cc4..59a1b31d371b 100644 --- a/crates/tauri-build/Cargo.toml +++ b/crates/tauri-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-build" -version = "2.5.6" +version = "2.6.1" description = "build time code to pair with https://crates.io/crates/tauri" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -26,9 +26,9 @@ targets = [ [dependencies] anyhow = "1" quote = { version = "1", optional = true } -tauri-codegen = { version = "2.5.5", path = "../tauri-codegen", optional = true } -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ - "build", +tauri-codegen = { version = "2.6.1", path = "../tauri-codegen", optional = true } +tauri-utils = { version = "2.9.1", path = "../tauri-utils", features = [ + "build-2", "resources", ] } cargo_toml = "0.22" @@ -41,7 +41,6 @@ tauri-winres = "0.3" semver = "1" dirs = "6" glob = "0.3" -toml = "0.9" schemars = { version = "1", features = ["preserve_order"] } [features] diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index 1516b337e1eb..39c39d60662d 100644 --- a/crates/tauri-build/src/lib.rs +++ b/crates/tauri-build/src/lib.rs @@ -226,6 +226,8 @@ fn default_windows_app_manifest() -> &'static str { #[derive(Debug)] pub struct WindowsAttributes { window_icon_path: Option, + /// Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets + static_vc_runtime: Option, /// A string containing an [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -253,6 +255,8 @@ pub struct WindowsAttributes { /// /// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests app_manifest: Option, + /// A series of strings containing additional .rc content to be appended to the generated resource file on Windows. + append_rc_content: Vec, } impl Default for WindowsAttributes { @@ -266,6 +270,8 @@ impl WindowsAttributes { pub fn new() -> Self { Self { window_icon_path: Default::default(), + static_vc_runtime: None, + append_rc_content: Vec::new(), app_manifest: Some(default_windows_app_manifest().into()), } } @@ -276,6 +282,8 @@ impl WindowsAttributes { Self { app_manifest: None, window_icon_path: Default::default(), + static_vc_runtime: None, + append_rc_content: Vec::new(), } } @@ -289,6 +297,15 @@ impl WindowsAttributes { self } + /// Sets whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets. + /// + /// If unset, this is read from `build > windows > staticVCRuntime` in the Tauri configuration. + #[must_use] + pub fn static_vc_runtime(mut self, static_vc_runtime: bool) -> Self { + self.static_vc_runtime.replace(static_vc_runtime); + self + } + /// Sets the [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -345,6 +362,14 @@ impl WindowsAttributes { self.app_manifest = Some(manifest.as_ref().to_string()); self } + + /// Append additional .rc content to the generated resource file on Windows. + /// This can be called multiple times to append multiple contents. + #[must_use] + pub fn append_rc_content>(mut self, content: S) -> Self { + self.append_rc_content.push(content.into()); + self + } } /// The attributes used on the build. @@ -422,7 +447,8 @@ impl Attributes { } pub fn is_dev() -> bool { - env::var("DEP_TAURI_DEV").expect("missing `cargo:dev` instruction, please update tauri to latest") + env::var_os("DEP_TAURI_DEV") + .expect("missing `cargo:dev` instruction, please update tauri to latest") == "true" } @@ -471,7 +497,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { println!("cargo:rerun-if-env-changed=TAURI_CONFIG"); - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_os = env::var_os("CARGO_CFG_TARGET_OS").unwrap(); let mobile = target_os == "ios" || target_os == "android"; cfg_alias("desktop", !mobile); cfg_alias("mobile", mobile); @@ -489,6 +515,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { json_patch::merge(&mut config, &merge_config); } let config: Config = serde_json::from_value(config)?; + let static_vc_runtime = should_static_link_vc_runtime(&config, &attributes); let s = config.identifier.split('.'); let last = s.clone().count() - 1; @@ -509,6 +536,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> { if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { mobile::generate_gradle_files(project_dir)?; + + // Update Android manifest with file associations + if let Some(associations) = config.bundle.file_associations.as_ref() { + mobile::update_android_manifest_file_associations(associations)?; + } } cfg_alias("dev", is_dev()); @@ -516,7 +548,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { let cargo_toml_path = Path::new("Cargo.toml").canonicalize()?; let mut manifest = Manifest::::from_path_with_metadata(cargo_toml_path)?; - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); manifest::check(&config, &mut manifest)?; @@ -625,12 +657,18 @@ pub fn try_build(attributes: Attributes) -> Result<()> { res.set_manifest(&manifest); } + for content in attributes.windows_attributes.append_rc_content { + res.append_rc_content(&content); + } + if let Some(version_str) = &config.version && let Ok(v) = Version::parse(version_str) { let version = (v.major << 48) | (v.minor << 32) | (v.patch << 16); res.set_version_info(VersionInfo::FILEVERSION, version); res.set_version_info(VersionInfo::PRODUCTVERSION, version); + res.set("FileVersion", version_str); + res.set("ProductVersion", version_str); } if let Some(product_name) = &config.product_name { @@ -699,10 +737,8 @@ pub fn try_build(attributes: Attributes) -> Result<()> { } } } - "msvc" => { - if env::var("STATIC_VCRUNTIME").is_ok_and(|v| v == "true") { - static_vcruntime::build(); - } + "msvc" if static_vc_runtime => { + static_vcruntime::build(); } _ => (), } @@ -715,3 +751,150 @@ pub fn try_build(attributes: Attributes) -> Result<()> { Ok(()) } + +fn to_winres_version(v: &semver::Version) -> u64 { + let build = v.build.parse::().map(u64::from).unwrap_or(0); + + (v.major << 48) | (v.minor << 32) | (v.patch << 16) | build +} + +fn should_static_link_vc_runtime(config: &Config, attributes: &Attributes) -> bool { + if let Some(value) = env::var_os("STATIC_VCRUNTIME") { + println!( + "cargo:warning=STATIC_VCRUNTIME is deprecated; use build.windows.staticVCRuntime in tauri.conf.json or tauri_build::WindowsAttributes::static_vc_runtime instead." + ); + value != "false" + } else { + attributes + .windows_attributes + .static_vc_runtime + .unwrap_or(config.build.windows.static_vc_runtime) + } +} + +#[cfg(test)] +mod tests { + use semver::Version; + + #[test] + fn version_uses_numeric_build_metadata() { + let version = Version::parse("1.2.3+42").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) | 42 + ); + } + + #[test] + fn version_ignores_non_numeric_composite_build_metadata() { + let version = Version::parse("1.2.3+42.sha").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) + ); + } + + #[test] + fn version_ignores_non_numeric_build_metadata() { + let version = Version::parse("1.2.3+abc").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) + ); + } + + #[test] + fn version_ignores_build_metadata_that_does_not_fit_in_u16() { + let version = Version::parse("1.2.3+70000").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) + ); + } + + #[test] + fn static_vc_runtime_chain() { + // 1. Nothing is set, should default to true + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 2. Set to anything but "false" in env, should be true + unsafe { std::env::set_var("STATIC_VCRUNTIME", "qweqe") }; + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + unsafe { std::env::remove_var("STATIC_VCRUNTIME") }; + + // 3. Set to "false" in env, should be false + unsafe { std::env::set_var("STATIC_VCRUNTIME", "false") }; + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + unsafe { std::env::remove_var("STATIC_VCRUNTIME") }; + + // 4. Set to true in attributes, should be true + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(true)); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 5. Set to false in attributes, should be false + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(false)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 6. Set to true in config, should be true + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: true, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 7. Set to false in config, should be false + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: false, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new(); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 8. Set to true in config and false in attributes, should be false because attributes takes precedence over config + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: true, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(false)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 9. Set to false in env and true in attributes, should be false because env takes precedence over attributes + unsafe { std::env::set_var("STATIC_VCRUNTIME", "false") }; + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(true)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + unsafe { std::env::remove_var("STATIC_VCRUNTIME") }; + } +} diff --git a/crates/tauri-build/src/manifest.rs b/crates/tauri-build/src/manifest.rs index 9e3b4fa63b89..6a72e70d68dc 100644 --- a/crates/tauri-build/src/manifest.rs +++ b/crates/tauri-build/src/manifest.rs @@ -23,7 +23,7 @@ struct AllowlistedDependency { name: String, alias: Option, kind: DependencyKind, - all_cli_managed_features: Option>, + all_cli_managed_features: Vec<&'static str>, expected_features: Vec, } @@ -33,7 +33,7 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> { name: "tauri-build".into(), alias: None, kind: DependencyKind::Build, - all_cli_managed_features: Some(vec!["isolation"]), + all_cli_managed_features: vec!["isolation"], expected_features: match config.app.security.pattern { PatternKind::Isolation { .. } => vec!["isolation".to_string()], _ => vec![], @@ -43,12 +43,10 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> { name: "tauri".into(), alias: None, kind: DependencyKind::Normal, - all_cli_managed_features: Some( - AppConfig::all_features() - .into_iter() - .filter(|f| f != &"tray-icon") - .collect(), - ), + all_cli_managed_features: AppConfig::all_features() + .into_iter() + .filter(|f| f != &"tray-icon") + .collect(), expected_features: config .app .features() @@ -129,23 +127,13 @@ fn check_features(dependency: Dependency, metadata: &AllowlistedDependency) -> R Dependency::Inherited(dep) => dep.features, }; - let diff = if let Some(all_cli_managed_features) = &metadata.all_cli_managed_features { - features_diff( - &features - .into_iter() - .filter(|f| all_cli_managed_features.contains(&f.as_str())) - .collect::>(), - &metadata.expected_features, - ) - } else { - features_diff( - &features - .into_iter() - .filter(|f| f.starts_with("allow-")) - .collect::>(), - &metadata.expected_features, - ) - }; + let diff = features_diff( + &features + .into_iter() + .filter(|f| metadata.all_cli_managed_features.contains(&f.as_str())) + .collect::>(), + &metadata.expected_features, + ); let mut error_message = String::new(); if !diff.remove.is_empty() { diff --git a/crates/tauri-build/src/mobile.rs b/crates/tauri-build/src/mobile.rs index 9acef2e917be..80f0613db8e4 100644 --- a/crates/tauri-build/src/mobile.rs +++ b/crates/tauri-build/src/mobile.rs @@ -2,10 +2,147 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; use anyhow::{Context, Result}; -use tauri_utils::write_if_changed; +use tauri_utils::{config::AndroidIntentAction, write_if_changed}; + +/// Updates the Android manifest to add file association intent filters +pub fn update_android_manifest_file_associations( + associations: &[tauri_utils::config::FileAssociation], +) -> Result<()> { + if associations.is_empty() { + return Ok(()); + } + + let intent_filters = generate_file_association_intent_filters(associations); + tauri_utils::build::update_android_manifest("tauri-file-associations", "activity", intent_filters) +} + +fn generate_file_association_intent_filters( + associations: &[tauri_utils::config::FileAssociation], +) -> String { + let mut filters = String::new(); + + for association in associations { + // Get mime types - use explicit mime_type, or infer from extensions + let mut mime_types = HashSet::new(); + + if let Some(mime_type) = &association.mime_type { + mime_types.insert(( + mime_type.clone(), + association.android_intent_action_filters.clone(), + )); + } else { + // Infer mime types from extensions + for ext in &association.ext { + if let Some(mime) = extension_to_mime_type(&ext.0) { + mime_types.insert((mime, association.android_intent_action_filters.clone())); + } + } + } + + // If we have mime types, create intent filters + if !mime_types.is_empty() { + for (mime_type, actions) in &mime_types { + filters.push_str("\n"); + if let Some(actions) = actions { + for action in actions { + let action = match action { + AndroidIntentAction::Send => "SEND", + AndroidIntentAction::SendMultiple => "SEND_MULTIPLE", + AndroidIntentAction::View => "VIEW", + _ => unimplemented!(), + }; + filters.push_str(&format!( + " \n" + )); + } + } else { + filters.push_str(" \n"); + filters.push_str(" \n"); + filters.push_str(" \n"); + } + filters.push_str(" \n"); + filters.push_str(" \n"); + filters.push_str(&format!( + " \n", + mime_type + )); + + // Add file scheme and path patterns for extensions + if !association.ext.is_empty() { + // Create path patterns for each extension + // Android's pathPattern needs \\. (double backslash-dot) in XML to match a literal dot + let path_patterns: Vec = association + .ext + .iter() + .map(|ext| format!(".*\\\\.{}", ext.0)) + .collect(); + + for pattern in &path_patterns { + filters.push_str(&format!( + " \n", + pattern + )); + } + } + + filters.push_str("\n"); + } + } else if !association.ext.is_empty() { + // If no mime type but we have extensions, use a generic approach + filters.push_str("\n"); + filters.push_str(" \n"); + filters.push_str(" \n"); + filters.push_str(" \n"); + + for ext in &association.ext { + // Android's pathPattern needs \\. (double backslash-dot) in XML to match a literal dot + filters.push_str(&format!( + " \n", + ext.0 + )); + } + + filters.push_str("\n"); + } + } + + filters +} + +fn extension_to_mime_type(ext: &str) -> Option { + Some( + match ext.to_lowercase().as_str() { + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "bmp" => "image/bmp", + "webp" => "image/webp", + "svg" => "image/svg+xml", + "ico" => "image/x-icon", + "tiff" | "tif" => "image/tiff", + "heic" | "heif" => "image/heic", + "mp4" => "video/mp4", + "mov" => "video/quicktime", + "avi" => "video/x-msvideo", + "mkv" => "video/x-matroska", + "mp3" => "audio/mpeg", + "wav" => "audio/wav", + "aac" => "audio/aac", + "m4a" => "audio/mp4", + "pdf" => "application/pdf", + "txt" => "text/plain", + "html" | "htm" => "text/html", + "json" => "application/json", + "xml" => "application/xml", + "rtf" => "application/rtf", + _ => return None, + } + .to_string(), + ) +} pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> { let gradle_settings_path = project_dir.join("tauri.settings.gradle"); @@ -15,7 +152,8 @@ pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> { "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string(); let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. val implementation by configurations -dependencies {" +dependencies { + implementation(\"androidx.lifecycle:lifecycle-process:2.10.0\")" .to_string(); for (env, value) in std::env::vars_os() { diff --git a/crates/tauri-bundler/CHANGELOG.md b/crates/tauri-bundler/CHANGELOG.md index b2fec9ab6a18..31a0e7f95a1f 100644 --- a/crates/tauri-bundler/CHANGELOG.md +++ b/crates/tauri-bundler/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## \[2.9.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.9.0] + +### New Features + +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. +- [`5a0ca7edb`](https://www.github.com/tauri-apps/tauri/commit/5a0ca7edbbc707199615a91845146e98b6f5e8ca) ([#14671](https://www.github.com/tauri-apps/tauri/pull/14671)) Added support to Liquid Glass icons. +- [`5dc2cee60`](https://www.github.com/tauri-apps/tauri/commit/5dc2cee60370665af88c185684432e425b1c987d) ([#14793](https://www.github.com/tauri-apps/tauri/pull/14793)) Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version` + + Notes: + + - For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION` + - For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change + +### Enhancements + +- [`be0e4bd2d`](https://www.github.com/tauri-apps/tauri/commit/be0e4bd2da02eb6cc75a8dc7c81663277e64c590) ([#15218](https://www.github.com/tauri-apps/tauri/pull/15218)) Added Vietnamese translations for the NSIS installer +- [`1035f12ee`](https://www.github.com/tauri-apps/tauri/commit/1035f12eeb8b23d9780881606d442d11c786e39e) ([#14923](https://www.github.com/tauri-apps/tauri/pull/14923)) Signtool path for windows arm systems was not being properly returned which caused failure in signing of windows binaries. + + This patch addresses it. + + Previously only the following were supported: + + - PROCESSOR_ARCHITECTURE_INTEL + - PROCESSOR_ARCHITECTURE_AMD64 + + The following were added: + + - PROCESSOR_ARCHITECTURE_ARM + - PROCESSOR_ARCHITECTURE_ARM64 + +### Bug Fixes + +- [`fcb702ec4`](https://www.github.com/tauri-apps/tauri/commit/fcb702ec4d924e81943efaeebea8d3edb7289c33) ([#14954](https://www.github.com/tauri-apps/tauri/pull/14954)) Fix `build --bundles` to allow `nsis` arg in linux+macOS +- [`c8d7003b2`](https://www.github.com/tauri-apps/tauri/commit/c8d7003b23657019a547fd7cdf3164834a28849a) ([#15102](https://www.github.com/tauri-apps/tauri/pull/15102)) Correct GitHub Release URL path for Linux i686 tooling. + +### What's Changed + +- [`9979cde1c`](https://www.github.com/tauri-apps/tauri/commit/9979cde1c5534dafb1a07cc4dc2bc280d15d2f66) ([#15175](https://www.github.com/tauri-apps/tauri/pull/15175)) Update NSIS installer Italian translations + +### Dependencies + +- Upgraded to `tauri-macos-sign@2.3.4` +- Upgraded to `tauri-utils@2.9.0` +- [`373b7e677`](https://www.github.com/tauri-apps/tauri/commit/373b7e677ec498899759de9fcd35941fe792b58b) ([#15177](https://www.github.com/tauri-apps/tauri/pull/15177)) Update Specta in lockfile and upgrade dependencies using the removed `doc_auto_cfg` attribute to fix errors building documentation + ## \[2.8.1] ### Bug Fixes diff --git a/crates/tauri-bundler/Cargo.toml b/crates/tauri-bundler/Cargo.toml index b8dc1373d282..34f6ef3e5601 100644 --- a/crates/tauri-bundler/Cargo.toml +++ b/crates/tauri-bundler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-bundler" -version = "2.8.1" +version = "2.9.1" authors = [ "George Burton ", "Tauri Programme within The Commons Conservancy", @@ -15,7 +15,7 @@ rust-version = "1.88" exclude = ["CHANGELOG.md", "/target", "rustfmt.toml"] [dependencies] -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ +tauri-utils = { version = "2.9.1", path = "../tauri-utils", features = [ "resources", ] } image = "0.25" @@ -50,7 +50,6 @@ plist = "1" [target."cfg(target_os = \"windows\")".dependencies] bitness = "0.4" windows-registry = "0.5" -glob = "0.3" [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.60" @@ -59,7 +58,7 @@ features = ["Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug"] [target."cfg(target_os = \"macos\")".dependencies] icns = { package = "tauri-icns", version = "0.1" } time = { version = "0.3", features = ["formatting"] } -tauri-macos-sign = { version = "2.3.3", path = "../tauri-macos-sign" } +tauri-macos-sign = { version = "2.3.4", path = "../tauri-macos-sign" } [target."cfg(target_os = \"linux\")".dependencies] heck = "0.5" @@ -67,6 +66,9 @@ ar = "0.9" md5 = "0.8" rpm = { version = "0.16", features = ["bzip2-compression"] } +[target."cfg(any(target_os = \"windows\", target_os = \"macos\", target_os = \"linux\"))".dependencies] +glob = "0.3" + [target."cfg(unix)".dependencies] which = "8" diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index d1166338cacf..dfb3d71b9de5 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -4,7 +4,6 @@ // SPDX-License-Identifier: MIT mod category; -#[cfg(any(target_os = "linux", target_os = "windows"))] mod kmp; #[cfg(target_os = "linux")] mod linux; @@ -15,28 +14,20 @@ mod settings; mod updater_bundle; mod windows; +pub use windows::vswhere_path; + use tauri_utils::{display_path, platform::Target as TargetPlatform}; -#[cfg(any(target_os = "linux", target_os = "windows"))] const BUNDLE_VAR_TOKEN: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK"; /// Patch a binary with bundle type information -#[cfg(any(target_os = "linux", target_os = "windows"))] fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> { - log::info!( - "Patching {} with bundle type information: {}", - display_path(binary), - package_type.short_name() - ); - - let mut file_data = std::fs::read(binary).expect("Could not read binary file."); - - let bundle_var_index = - kmp::index_of(BUNDLE_VAR_TOKEN, &file_data).ok_or(crate::Error::MissingBundleTypeVar)?; #[cfg(target_os = "linux")] let bundle_type = match package_type { crate::PackageType::Deb => b"__TAURI_BUNDLE_TYPE_VAR_DEB", crate::PackageType::Rpm => b"__TAURI_BUNDLE_TYPE_VAR_RPM", crate::PackageType::AppImage => b"__TAURI_BUNDLE_TYPE_VAR_APP", + // NSIS installers can be built in linux using cargo-xwin + crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", _ => { return Err(crate::Error::InvalidPackageType( package_type.short_name().to_owned(), @@ -55,7 +46,31 @@ fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<( )); } }; + #[cfg(target_os = "macos")] + let bundle_type = match package_type { + // NSIS installers can be built in macOS using cargo-xwin + crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", + crate::PackageType::MacOsBundle | crate::PackageType::Dmg => { + // skip patching for macOS-native bundles + return Ok(()); + } + _ => { + return Err(crate::Error::InvalidPackageType( + package_type.short_name().to_owned(), + "macOS".to_owned(), + )); + } + }; + log::info!( + "Patching {} with bundle type information: {}", + display_path(binary), + package_type.short_name() + ); + + let mut file_data = std::fs::read(binary).expect("Could not read binary file."); + let bundle_var_index = + kmp::index_of(BUNDLE_VAR_TOKEN, &file_data).ok_or(crate::Error::MissingBundleTypeVar)?; file_data[bundle_var_index..bundle_var_index + BUNDLE_VAR_TOKEN.len()] .copy_from_slice(bundle_type); @@ -135,7 +150,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { continue; } - #[cfg(any(target_os = "linux", target_os = "windows"))] if let Err(e) = patch_binary(&main_binary_path, package_type) { log::warn!( "Failed to add bundler type to the binary: {e}. Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues" @@ -167,7 +181,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { #[cfg(target_os = "windows")] PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?, - // note: don't restrict to windows as NSIS installers can be built in linux using cargo-xwin + // don't restrict to windows as NSIS installers can be built in linux+macOS using cargo-xwin PackageType::Nsis => windows::nsis::bundle_project(settings, false)?, #[cfg(target_os = "linux")] diff --git a/crates/tauri-bundler/src/bundle/kmp/mod.rs b/crates/tauri-bundler/src/bundle/kmp/mod.rs index 07df0c5901fa..da54a7645102 100644 --- a/crates/tauri-bundler/src/bundle/kmp/mod.rs +++ b/crates/tauri-bundler/src/bundle/kmp/mod.rs @@ -5,7 +5,6 @@ // Knuth-Morris-Pratt algorithm // based on https://github.com/howeih/rust_kmp -#[cfg(any(target_os = "linux", target_os = "windows"))] pub fn index_of(pattern: &[u8], target: &[u8]) -> Option { let failure_function = find_failure_function(pattern); @@ -38,7 +37,6 @@ pub fn index_of(pattern: &[u8], target: &[u8]) -> Option { None } -#[cfg(any(target_os = "linux", target_os = "windows"))] fn find_failure_function(pattern: &[u8]) -> Vec { let mut i = 1; let mut j = 0; diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs index 36883081c18b..0014525d512f 100644 --- a/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs +++ b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs @@ -232,7 +232,7 @@ fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result< write_and_make_executable(&apprun, data)?; } - let linuxdeploy_arch = if arch == "i686" { "i383" } else { arch }; + let linuxdeploy_arch = if arch == "i686" { "i386" } else { arch }; let linuxdeploy = tools_path.join(format!("linuxdeploy-{linuxdeploy_arch}.AppImage")); if !linuxdeploy.exists() { let data = download(&format!( diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs b/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs index 3c64244b7f68..743e85079a59 100644 --- a/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs +++ b/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs @@ -45,14 +45,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { fs::create_dir_all(&tools_path)?; - // TODO: mirror let quick_sharun = tools_path.join("quick-sharun.sh"); - if !quick_sharun.exists() { - let data = download( - "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh", - )?; - write_and_make_executable(&quick_sharun, data)?; - } + // TODO: offline build support + // github doesn't send a Last-Modified header + // if !quick_sharun.exists() {} + let data = download( + "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh", + )?; + write_and_make_executable(&quick_sharun, data)?; let package_dir = settings .project_out_directory() @@ -167,6 +167,13 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .map(|b| format!(" \"{}\"", b.to_string_lossy())) .collect::(); + // quick-sharun checks the main binary with ldd so even though we manually add the cef files, + // we'll add them to LD_LIBRARY_PATH to pass the pre-bundle checks + let mut ld_lib_path = data_dir.join("usr/lib/").to_string_lossy().to_string(); + if let Ok(ld_env) = std::env::var("LD_LIBRARY_PATH") { + ld_lib_path = format!("{}:{}", ld_lib_path, ld_env); + } + // TODO: Consider to not rely on quick-sharun when we have more time Command::new("/bin/sh") .current_dir(&output_path) @@ -178,9 +185,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { ) .env("ICON", &larger_icon.path) .env("OUTPUT_APPIMAGE", "1") - .env("URUNTIME2APPIMAGE_SOURCE", "https://raw.githubusercontent.com/FabianLars/Anylinux-AppImages/refs/heads/main/useful-tools/uruntime2appimage.sh") + .env("HOOKSRC", "https://raw.githubusercontent.com/FabianLars/Anylinux-AppImages/refs/heads/main/useful-tools/hooks") .env("DEPLOY_CHROMIUM", "1") .env("ADD_HOOKS", "fix-namespaces.hook") + .env("LD_LIBRARY_PATH", ld_lib_path) .args([ "-c", &format!( diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index ff3dbdf86b43..e5eee1d14f81 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -115,20 +115,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { settings.copy_resources(&resources_dir)?; - let bin_paths = settings - .copy_binaries(&bin_dir) - .with_context(|| "Failed to copy external binaries")?; - sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { - path, - is_an_executable: true, - })); - - let bin_paths = copy_binaries_to_bundle(&bundle_directory, settings)?; - sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { - path, - is_an_executable: true, - })); - copy_custom_files_to_bundle(&bundle_directory, settings)?; // Handle CEF support if cef_path is set @@ -148,6 +134,23 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { ); } + // Sign CEF nested code first (helper apps/framework internals), then top-level binaries. + // Signing the main executable before CEF helpers causes codesign to fail with: + // "code object is not signed at all ... In subcomponent: ... Helper (Renderer).app". + let bin_paths = settings + .copy_binaries(&bin_dir) + .with_context(|| "Failed to copy external binaries")?; + sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { + path, + is_an_executable: true, + })); + + let bin_paths = copy_binaries_to_bundle(&bundle_directory, settings)?; + sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { + path, + is_an_executable: true, + })); + if settings.no_sign() { log::warn!("Skipping signing due to --no-sign flag.",); } else if let Some(keychain) = @@ -297,103 +300,14 @@ fn create_info_plist( plist.insert("LSMinimumSystemVersion".into(), version.into()); } - if let Some(associations) = settings.file_associations() { - let exported_associations = associations - .iter() - .filter_map(|association| { - association.exported_type.as_ref().map(|exported_type| { - let mut dict = plist::Dictionary::new(); - - dict.insert( - "UTTypeIdentifier".into(), - exported_type.identifier.clone().into(), - ); - if let Some(description) = &association.description { - dict.insert("UTTypeDescription".into(), description.clone().into()); - } - if let Some(conforms_to) = &exported_type.conforms_to { - dict.insert( - "UTTypeConformsTo".into(), - plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()), - ); - } - - let mut specification = plist::Dictionary::new(); - specification.insert( - "public.filename-extension".into(), - plist::Value::Array( - association - .ext - .iter() - .map(|s| s.to_string().into()) - .collect(), - ), - ); - if let Some(mime_type) = &association.mime_type { - specification.insert("public.mime-type".into(), mime_type.clone().into()); - } - - dict.insert("UTTypeTagSpecification".into(), specification.into()); - - plist::Value::Dictionary(dict) - }) - }) - .collect::>(); - - if !exported_associations.is_empty() { - plist.insert( - "UTExportedTypeDeclarations".into(), - plist::Value::Array(exported_associations), - ); + if let Some(associations) = settings.file_associations() + && let Some(file_associations_plist) = + tauri_utils::config::file_associations_plist(associations) + && let Some(plist_dict) = file_associations_plist.as_dictionary() + { + for (key, value) in plist_dict { + plist.insert(key.clone(), value.clone()); } - - plist.insert( - "CFBundleDocumentTypes".into(), - plist::Value::Array( - associations - .iter() - .map(|association| { - let mut dict = plist::Dictionary::new(); - - if !association.ext.is_empty() { - dict.insert( - "CFBundleTypeExtensions".into(), - plist::Value::Array( - association - .ext - .iter() - .map(|ext| ext.to_string().into()) - .collect(), - ), - ); - } - - if let Some(content_types) = &association.content_types { - dict.insert( - "LSItemContentTypes".into(), - plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()), - ); - } - - dict.insert( - "CFBundleTypeName".into(), - association - .name - .as_ref() - .unwrap_or(&association.ext[0].0) - .to_string() - .into(), - ); - dict.insert( - "CFBundleTypeRole".into(), - association.role.to_string().into(), - ); - dict.insert("LSHandlerRank".into(), association.rank.to_string().into()); - plist::Value::Dictionary(dict) - }) - .collect(), - ), - ); } if let Some(path) = bundle_icon_file { diff --git a/crates/tauri-bundler/src/bundle/macos/icon.rs b/crates/tauri-bundler/src/bundle/macos/icon.rs index 63b6fe63adb0..25bff4fa1afe 100644 --- a/crates/tauri-bundler/src/bundle/macos/icon.rs +++ b/crates/tauri-bundler/src/bundle/macos/icon.rs @@ -65,7 +65,7 @@ pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result crate::Result< let icon_path = icon_path?; if icon_path .extension() - .map_or(false, |ext| ext == "png" || ext == "car") + .is_some_and(|ext| ext == "png" || ext == "car") { continue; } else if icon_path.extension() == Some(OsStr::new("icns")) { diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index c7447b91d54e..53dc8182000c 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -126,7 +126,7 @@ const ALL_PACKAGE_TYPES: &[PackageType] = &[ PackageType::IosBundle, #[cfg(target_os = "windows")] PackageType::WindowsMsi, - #[cfg(target_os = "windows")] + // NSIS installers can be built on all platforms but it's hidden in the --help output on macOS/Linux. PackageType::Nsis, #[cfg(target_os = "macos")] PackageType::MacOsBundle, @@ -470,11 +470,19 @@ pub struct NsisSettings { pub sidebar_image: Option, /// The path to an icon file used as the installer icon. pub installer_icon: Option, + /// The path to an icon file used as the uninstaller icon. + pub uninstaller_icon: Option, + /// The path to a bitmap file to display on the header of uninstallers pages. + /// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image` + /// + /// The recommended dimensions are 150px x 57px. + pub uninstaller_header_image: Option, /// Whether the installation will be for all users or just the current user. pub install_mode: NSISInstallerMode, - /// A list of installer languages. + /// A list of installer languages. Default to `["English"]` if not set. + /// /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. - /// To allow the user to select the language, set `display_language_selector` to `true`. + /// To allow the user to select the language, set [`Self::display_language_selector`] to `true`. /// /// See for the complete list of languages. pub languages: Option>, @@ -483,7 +491,7 @@ pub struct NsisSettings { /// /// See for an example `.nsi` file. /// - /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array, + /// **Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array, pub custom_language_files: Option>, /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. /// By default the OS language is selected, with a fallback to the first language in the `languages` array. @@ -532,6 +540,10 @@ pub struct NsisSettings { /// Try to ensure that the WebView2 version is equal to or newer than this version, /// if the user's WebView2 is older than this version, /// the installer will try to trigger a WebView2 update. + #[deprecated( + since = "2.8.0", + note = "Use `WindowsSettings::minimum_webview2_version` instead." + )] pub minimum_webview2_version: Option, } @@ -587,6 +599,17 @@ pub struct WindowsSettings { /// if you are on another platform and want to cross-compile and sign you will /// need to use another tool like `osslsigncode`. pub sign_command: Option, + /// Try to ensure that the WebView2 version is equal to or newer than this version, + /// if the user's WebView2 is older than this version, + /// the installer will try to trigger a WebView2 update. + pub minimum_webview2_version: Option, + /// Whether to bundle the Visual C++ runtime DLLs alongside the application. + /// + /// This can be particularly useful when the application includes sidecars or DLLs that do not + /// statically link the Visual C++ runtime and require the runtime DLLs at runtime, and users + /// should not be required to install the Visual C++ Redistributable. This can also be useful + /// when `static_vc_runtime` is set to `false`. + pub bundle_vc_runtime: bool, } impl WindowsSettings { @@ -612,6 +635,8 @@ mod _default { webview_install_mode: Default::default(), allow_downgrades: true, sign_command: None, + minimum_webview2_version: None, + bundle_vc_runtime: false, } } } diff --git a/crates/tauri-bundler/src/bundle/windows/mod.rs b/crates/tauri-bundler/src/bundle/windows/mod.rs index b92fb5f56216..ab2e4594963a 100644 --- a/crates/tauri-bundler/src/bundle/windows/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/mod.rs @@ -12,5 +12,5 @@ pub mod sign; mod util; pub use util::{ NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME, - WIX_UPDATER_OUTPUT_FOLDER_NAME, + WIX_UPDATER_OUTPUT_FOLDER_NAME, vswhere_path, }; diff --git a/crates/tauri-bundler/src/bundle/windows/msi/main.wxs b/crates/tauri-bundler/src/bundle/windows/msi/main.wxs index a08c3b2cc5dc..dec39dadb4bc 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/main.wxs +++ b/crates/tauri-bundler/src/bundle/windows/msi/main.wxs @@ -283,38 +283,62 @@ {{#if install_webview}} - - - + + + + {{#if download_bootstrapper}} + - + {{/if}} - {{#if webview2_bootstrapper_path}} + - + {{/if}} - {{#if webview2_installer_path}} + - + + + + {{/if}} + + {{#if minimum_webview2_version}} + + + + + + + + + + + + + {{/if}} diff --git a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs index 266a6a56036a..78248bffeae5 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs @@ -10,7 +10,7 @@ use crate::{ sign::{should_sign, try_sign}, util::{ WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME, download_webview2_bootstrapper, - download_webview2_offline_installer, + download_webview2_offline_installer, vc_runtime_dlls, }, }, }, @@ -536,6 +536,13 @@ pub fn build_wix_app_installer( } } + if let Some(minimum_webview2_version) = &settings.windows().minimum_webview2_version { + data.insert( + "minimum_webview2_version", + to_json(minimum_webview2_version), + ); + } + if let Some(license) = settings.license_file() { if license.ends_with(".rtf") { data.insert("license", to_json(license)); @@ -1089,6 +1096,35 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { } } + let mut dlls = Vec::new(); + + if settings.windows().bundle_vc_runtime { + for dll in vc_runtime_dlls(settings.binary_arch())? { + let resource_path = dunce::simplified(&dll).to_path_buf(); + if added_resources.contains(&resource_path) { + continue; + } + added_resources.push(resource_path.clone()); + dlls.push(ResourceFile { + id: format!("I{}", Uuid::new_v4().as_simple()), + guid: Uuid::new_v4().to_string(), + path: resource_path, + }); + } + } + + if !dlls.is_empty() { + resources + .entry("".to_string()) + .and_modify(|r| r.files.append(&mut dlls)) + .or_insert(ResourceDirectory { + path: "".to_string(), + name: "".to_string(), + directories: vec![], + files: dlls, + }); + } + // Handle CEF support if cef_path is set, // using https://github.com/chromiumembedded/cef/blob/master/tools/distrib/win/README.redistrib.txt as a reference if let Some(cef_path) = settings.bundle_settings().cef_path.as_ref() { diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi b/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi index e178c49ea12c..a48a46149f6d 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi +++ b/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi @@ -41,6 +41,8 @@ ${StrLoc} !define INSTALLERICON "{{installer_icon}}" !define SIDEBARIMAGE "{{sidebar_image}}" !define HEADERIMAGE "{{header_image}}" +!define UNINSTALLERICON "{{uninstaller_icon}}" +!define UNINSTALLERHEADERIMAGE "{{uninstaller_header_image}}" !define MAINBINARYNAME "{{main_binary_name}}" !define MAINBINARYSRCPATH "{{main_binary_path}}" !define BUNDLEID "{{bundle_id}}" @@ -129,10 +131,26 @@ VIAddVersionKey "ProductVersion" "${VERSION}" !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" !endif -; Installer header image +; Enable header images for installer and uninstaller pages when either image is configured. !if "${HEADERIMAGE}" != "" !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" +!else if "${UNINSTALLERHEADERIMAGE}" != "" + !define MUI_HEADERIMAGE +!endif + +; Installer header image +!if "${HEADERIMAGE}" != "" + !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" +!endif + +; Uninstaller header image +!if "${UNINSTALLERHEADERIMAGE}" != "" + !define MUI_HEADERIMAGE_UNBITMAP "${UNINSTALLERHEADERIMAGE}" +!endif + +; Uninstaller icon +!if "${UNINSTALLERICON}" != "" + !define MUI_UNICON "${UNINSTALLERICON}" !endif ; Define registry key to store installer language diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh index bca96544da58..f505e4e8ec98 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh +++ b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh @@ -1,27 +1,27 @@ -LangString addOrReinstall ${LANG_ITALIAN} "Aggiungi/Reinstalla componenti" -LangString alreadyInstalled ${LANG_ITALIAN} "Già installato" -LangString alreadyInstalledLong ${LANG_ITALIAN} "${PRODUCTNAME} ${VERSION} è già installato. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare." -LangString appRunning ${LANG_ITALIAN} "{{product_name}} è in esecuzione! Chiudi e poi riprova." -LangString appRunningOkKill ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nSeleziona OK per chiuderlo" -LangString chooseMaintenanceOption ${LANG_ITALIAN} "Seleziona l'operazione di manutenzione da eseguire." -LangString choowHowToInstall ${LANG_ITALIAN} "Seleziona come vuoi installare ${PRODUCTNAME}." -LangString createDesktop ${LANG_ITALIAN} "Crea scorciatoia sul Desktop" +LangString addOrReinstall ${LANG_ITALIAN} "Aggiungi/reinstalla componenti" +LangString alreadyInstalled ${LANG_ITALIAN} "Programma già installato" +LangString alreadyInstalledLong ${LANG_ITALIAN} "${PRODUCTNAME} ${VERSION} è già installato.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'." +LangString appRunning ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nChiudi {product_name}} e riprova." +LangString appRunningOkKill ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nPer chiudere {product_name} seleziona 'OK'" +LangString chooseMaintenanceOption ${LANG_ITALIAN} "Scegli l'operazione di manutenzione da eseguire." +LangString choowHowToInstall ${LANG_ITALIAN} "Scegli come vuoi installare ${PRODUCTNAME}." +LangString createDesktop ${LANG_ITALIAN} "Crea collegamento sul desktop" LangString dontUninstall ${LANG_ITALIAN} "Non disinstallare" -LangString dontUninstallDowngrade ${LANG_ITALIAN} "Non disinstallare (Il downgrade senza la disinstallazione è disabilitato per questo installer)" -LangString failedToKillApp ${LANG_ITALIAN} "Impossibile chiudere {{product_name}}. Chiudi e poi riprova" -LangString installingWebview2 ${LANG_ITALIAN} "Installando WebView2..." -LangString newerVersionInstalled ${LANG_ITALIAN} "Una versione più recente di ${PRODUCTNAME} è già installata! Non è consigliato installare una versione più vecchia. Se vuoi comunque procedere, è meglio prima disinstallare la versione corrente. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare." +LangString dontUninstallDowngrade ${LANG_ITALIAN} "Non disinstallare (per questo installer il downgrade senza la disinstallazione è disabilitato)" +LangString failedToKillApp ${LANG_ITALIAN} "Impossibile chiudere {{product_name}}.$\nChiudi {product_name} e poi riprova" +LangString installingWebview2 ${LANG_ITALIAN} "Installazione WebView2..." +LangString newerVersionInstalled ${LANG_ITALIAN} "È già installata una versione più recente di ${PRODUCTNAME}!$\nNon è consigliato installare una versione più vecchia.$\nSe vuoi comunque procedere, è meglio prima disinstallare la versione attuale.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'." LangString older ${LANG_ITALIAN} "più vecchia" -LangString olderOrUnknownVersionInstalled ${LANG_ITALIAN} "Una versione $R4 di ${PRODUCTNAME} è installata nel tuo sistema. È consigliato che disinstalli la versione corrente prima di procedere all'installazione. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare." -LangString silentDowngrades ${LANG_ITALIAN} "I downgrade sono disabilitati per questo installer, impossibile procedere con l'installer silenzioso, usa invece l'installer con interfaccia grafica.$\n" +LangString olderOrUnknownVersionInstalled ${LANG_ITALIAN} "Nel sistema è installata una versione $R4 di ${PRODUCTNAME}.$\nPrima di procedere all'installazione è consigliabile disinstallare la versione attuale.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'." +LangString silentDowngrades ${LANG_ITALIAN} "Per questo installer i downgrade sono disabilitati , impossibile procedere con l'installer silenzioso, usa invece l'installer con interfaccia grafica.$\n" LangString unableToUninstall ${LANG_ITALIAN} "Impossibile disinstallare!" LangString uninstallApp ${LANG_ITALIAN} "Disinstalla ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_ITALIAN} "Disinstalla prima di installare" LangString unknown ${LANG_ITALIAN} "sconosciuta" -LangString webview2AbortError ${LANG_ITALIAN} "Errore nell'installazione di WebView2! L'app non può funzionare senza. Prova a riavviare l'installer." -LangString webview2DownloadError ${LANG_ITALIAN} "Errore: Il download di WebView2 è fallito - $0" -LangString webview2DownloadSuccess ${LANG_ITALIAN} "Bootstrapper WebView2 scaricato con successo" -LangString webview2Downloading ${LANG_ITALIAN} "Scaricando il bootstrapper WebView2..." -LangString webview2InstallError ${LANG_ITALIAN} "Errore: L'installazione di WebView2 è fallita con il codice $1" +LangString webview2AbortError ${LANG_ITALIAN} "Errore nell'installazione di WebView2!$\nL'app non può funzionare senza.$\nProva a riavviare l'installer." +LangString webview2DownloadError ${LANG_ITALIAN} "Errore: il download di WebView2 è fallito - $0" +LangString webview2DownloadSuccess ${LANG_ITALIAN} "Download bootstrapper WebView2 completato" +LangString webview2Downloading ${LANG_ITALIAN} "Download bootstrapper WebView2..." +LangString webview2InstallError ${LANG_ITALIAN} "Errore: l'installazione di WebView2 è fallita con codice errore $1" LangString webview2InstallSuccess ${LANG_ITALIAN} "WebView2 installato correttamente" LangString deleteAppData ${LANG_ITALIAN} "Cancella i dati dell'applicazione" diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh new file mode 100644 index 000000000000..00bd156ccf79 --- /dev/null +++ b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh @@ -0,0 +1,27 @@ +LangString addOrReinstall ${LANG_VIETNAMESE} "Thêm/Cài đặt lại các thành phần" +LangString alreadyInstalled ${LANG_VIETNAMESE} "Đã được cài đặt" +LangString alreadyInstalledLong ${LANG_VIETNAMESE} "${PRODUCTNAME} ${VERSION} đã được cài đặt. Hãy chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục." +LangString appRunning ${LANG_VIETNAMESE} "{{product_name}} đang chạy! Vui lòng đóng ứng dụng trước rồi thử lại." +LangString appRunningOkKill ${LANG_VIETNAMESE} "{{product_name}} đang chạy!$\nNhấn OK để tắt ứng dụng" +LangString chooseMaintenanceOption ${LANG_VIETNAMESE} "Chọn thao tác bảo trì bạn muốn thực hiện." +LangString choowHowToInstall ${LANG_VIETNAMESE} "Chọn cách bạn muốn cài đặt ${PRODUCTNAME}." +LangString createDesktop ${LANG_VIETNAMESE} "Tạo biểu tượng ngoài màn hình" +LangString dontUninstall ${LANG_VIETNAMESE} "Không gỡ cài đặt" +LangString dontUninstallDowngrade ${LANG_VIETNAMESE} "Không gỡ cài đặt (Không hỗ trợ hạ cấp mà không gỡ cài đặt trong bộ cài này)" +LangString failedToKillApp ${LANG_VIETNAMESE} "Không thể tắt {{product_name}}. Vui lòng đóng ứng dụng trước rồi thử lại" +LangString installingWebview2 ${LANG_VIETNAMESE} "Đang cài đặt WebView2..." +LangString newerVersionInstalled ${LANG_VIETNAMESE} "Một phiên bản mới hơn của ${PRODUCTNAME} đã được cài đặt! Không khuyến nghị cài đặt phiên bản cũ hơn. Nếu bạn vẫn muốn cài phiên bản này, hãy gỡ phiên bản hiện tại trước. Chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục." +LangString older ${LANG_VIETNAMESE} "cũ hơn" +LangString olderOrUnknownVersionInstalled ${LANG_VIETNAMESE} "Một phiên bản $R4 của ${PRODUCTNAME} đã được cài trên hệ thống. Khuyến nghị gỡ phiên bản hiện tại trước khi cài đặt. Chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục." +LangString silentDowngrades ${LANG_VIETNAMESE} "Không hỗ trợ hạ cấp trong chế độ cài đặt im lặng, không thể tiếp tục. Vui lòng sử dụng trình cài đặt giao diện đồ họa.$\n" +LangString unableToUninstall ${LANG_VIETNAMESE} "Không thể gỡ cài đặt!" +LangString uninstallApp ${LANG_VIETNAMESE} "Gỡ cài đặt ${PRODUCTNAME}" +LangString uninstallBeforeInstalling ${LANG_VIETNAMESE} "Gỡ cài đặt trước khi cài đặt" +LangString unknown ${LANG_VIETNAMESE} "không xác định" +LangString webview2AbortError ${LANG_VIETNAMESE} "Cài đặt WebView2 thất bại! Ứng dụng không thể chạy nếu thiếu thành phần này. Hãy thử khởi động lại trình cài đặt." +LangString webview2DownloadError ${LANG_VIETNAMESE} "Lỗi: Tải WebView2 thất bại - $0" +LangString webview2DownloadSuccess ${LANG_VIETNAMESE} "Tải trình cài đặt WebView2 thành công" +LangString webview2Downloading ${LANG_VIETNAMESE} "Đang tải trình cài đặt WebView2..." +LangString webview2InstallError ${LANG_VIETNAMESE} "Lỗi: Cài đặt WebView2 thất bại với mã lỗi $1" +LangString webview2InstallSuccess ${LANG_VIETNAMESE} "Cài đặt WebView2 thành công" +LangString deleteAppData ${LANG_VIETNAMESE} "Xóa dữ liệu ứng dụng" diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index 718311e8ff0d..322bc7036985 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -10,7 +10,7 @@ use crate::{ sign::{should_sign, sign_command, try_sign}, util::{ NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, download_webview2_bootstrapper, - download_webview2_offline_installer, + download_webview2_offline_installer, vc_runtime_dlls, }, }, }, @@ -353,6 +353,20 @@ fn build_nsis_app_installer( ); } + if let Some(uninstaller_icon) = &nsis.uninstaller_icon { + data.insert( + "uninstaller_icon", + to_json(dunce::canonicalize(uninstaller_icon)?), + ); + } + + if let Some(uninstaller_header_image) = &nsis.uninstaller_header_image { + data.insert( + "uninstaller_header_image", + to_json(dunce::canonicalize(uninstaller_header_image)?), + ); + } + if let Some(installer_hooks) = &nsis.installer_hooks { let installer_hooks = dunce::canonicalize(installer_hooks)?; data.insert("installer_hooks", to_json(installer_hooks)); @@ -361,7 +375,12 @@ fn build_nsis_app_installer( if let Some(start_menu_folder) = &nsis.start_menu_folder { data.insert("start_menu_folder", to_json(start_menu_folder)); } - if let Some(minimum_webview2_version) = &nsis.minimum_webview2_version { + #[allow(deprecated)] + if let Some(minimum_webview2_version) = nsis + .minimum_webview2_version + .as_ref() + .or(settings.windows().minimum_webview2_version.as_ref()) + { data.insert( "minimum_webview2_version", to_json(minimum_webview2_version), @@ -758,6 +777,22 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { } } + if settings.windows().bundle_vc_runtime { + for dll in vc_runtime_dlls(settings.binary_arch())? { + let dll = dunce::simplified(&dll).to_path_buf(); + if added_resources.contains(&dll) { + continue; + } + let target = PathBuf::from( + dll + .file_name() + .expect("failed to extract Visual C++ runtime DLL filename"), + ); + added_resources.push(dll.clone()); + resources.insert(dll, (PathBuf::new(), target)); + } + } + // Handle CEF support if cef_path is set, // using https://github.com/chromiumembedded/cef/blob/master/tools/distrib/win/README.redistrib.txt as a reference if let Some(cef_path) = settings.bundle_settings().cef_path.as_ref() { @@ -940,6 +975,7 @@ fn get_lang_data(lang: &str) -> Option<(String, &[u8])> { "portuguese" => include_bytes!("./languages/Portuguese.nsh"), "ukrainian" => include_bytes!("./languages/Ukrainian.nsh"), "norwegian" => include_bytes!("./languages/Norwegian.nsh"), + "vietnamese" => include_bytes!("./languages/Vietnamese.nsh"), _ => return None, }; Some((path, content)) diff --git a/crates/tauri-bundler/src/bundle/windows/sign.rs b/crates/tauri-bundler/src/bundle/windows/sign.rs index 457481a46da3..bcc5cbcee3cb 100644 --- a/crates/tauri-bundler/src/bundle/windows/sign.rs +++ b/crates/tauri-bundler/src/bundle/windows/sign.rs @@ -97,7 +97,7 @@ fn signtool() -> Option { kit_bin_paths.push(kits_root_10_bin_path); // Choose which version of SignTool to use based on OS bitness - let arch_dir = util::os_bitness().ok_or(crate::Error::UnsupportedBitness)?; + let arch_dir = util::processor_architecture().ok_or(crate::Error::UnsupportedBitness)?; /* Iterate through all bin paths, checking for existence of a SignTool executable. */ for kit_bin_path in &kit_bin_paths { diff --git a/crates/tauri-bundler/src/bundle/windows/util.rs b/crates/tauri-bundler/src/bundle/windows/util.rs index 55cfea3a968f..4e606f04e090 100644 --- a/crates/tauri-bundler/src/bundle/windows/util.rs +++ b/crates/tauri-bundler/src/bundle/windows/util.rs @@ -2,12 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#[cfg(windows)] +use std::process::Command; use std::{ - fs::create_dir_all, + fs, + io::Write, path::{Path, PathBuf}, }; use ureq::ResponseExt; +use crate::bundle::settings::Arch; use crate::utils::http_utils::{base_ureq_agent, download}; pub const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; @@ -22,6 +26,11 @@ pub const NSIS_UPDATER_OUTPUT_FOLDER_NAME: &str = "nsis-updater"; pub const WIX_OUTPUT_FOLDER_NAME: &str = "msi"; pub const WIX_UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater"; +const VSWHERE: &[u8] = include_bytes!("vswhere.exe"); +const VCTOOLS_REDIST_DIR_ENV_VAR: &str = "VCTOOLS_REDIST_DIR"; +#[cfg(windows)] +const VC_REDIST_COMPONENT: &str = "Microsoft.VisualStudio.Component.VC.Redist.14.Latest"; + pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> { let agent = base_ureq_agent(); let response = agent.head(url).call().map_err(Box::new)?; @@ -57,16 +66,173 @@ pub fn download_webview2_offline_installer(base_path: &Path, arch: &str) -> crat let dir_path = base_path.join(guid); let file_path = dir_path.join(filename); if !file_path.exists() { - create_dir_all(dir_path)?; + fs::create_dir_all(dir_path)?; std::fs::write(&file_path, download(url)?)?; } Ok(file_path) } +/// Finds the Visual C++ runtime DLLs for the given architecture. +pub fn vc_runtime_dlls(arch: Arch) -> crate::Result> { + let arch = vc_runtime_arch(arch)?; + let redist_dir = vc_redist_dir()?; + let runtime_dir = vc_runtime_dir(&redist_dir, arch)?; + + let dlls = glob::glob(&glob_path(&runtime_dir, "*.dll"))?.collect::, _>>()?; + if dlls.is_empty() { + return Err(crate::Error::GenericError(format!( + "no Visual C++ runtime DLLs found in `{}`", + runtime_dir.display() + ))); + } + + Ok(dlls) +} + +#[inline(always)] +fn vc_runtime_arch(arch: Arch) -> crate::Result<&'static str> { + match arch { + Arch::X86_64 => Ok("x64"), + Arch::X86 => Ok("x86"), + Arch::AArch64 => Ok("arm64"), + _ => Err(crate::Error::GenericError( + "bundling the Visual C++ runtime is only supported for Windows x86, x64 and arm64 targets" + .into(), + )), + } +} + +#[cfg(windows)] +fn visual_studio_dir() -> crate::Result { + let Some(vswhere) = vswhere_path() else { + return Err(crate::Error::GenericError( + "failed to prepare bundled vswhere.exe".into(), + )); + }; + + let output = Command::new(vswhere) + .args([ + "-latest", + "-prerelease", + "-products", + "*", + "-requires", + VC_REDIST_COMPONENT, + "-property", + "installationPath", + "-format", + "value", + "-utf8", + ]) + .output()?; + + if !output.status.success() { + return Err(crate::Error::GenericError(format!( + "failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component" + ))); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let Some(vs_dir) = stdout.lines().map(str::trim).find(|line| !line.is_empty()) else { + return Err(crate::Error::GenericError(format!( + "failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component" + ))); + }; + + Ok(PathBuf::from(vs_dir)) +} + +fn vc_redist_dir() -> crate::Result { + if let Ok(redist_dir) = std::env::var(VCTOOLS_REDIST_DIR_ENV_VAR) { + return Ok(PathBuf::from(redist_dir)); + } + + #[cfg(windows)] + { + let vs_dir = visual_studio_dir()?; + Ok(vs_dir.join("VC/Redist/MSVC")) + } + + #[cfg(not(windows))] + { + Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime redist directory; set {VCTOOLS_REDIST_DIR_ENV_VAR} when bundling the Visual C++ runtime from non-Windows hosts" + ))) + } +} + +fn vc_runtime_dir(redist_dir: &Path, arch: &str) -> crate::Result { + let Some(latest_version_dir) = latest_vc_redist_version_dir(redist_dir)? else { + return Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime versions in `{}`", + redist_dir.display() + ))); + }; + + let arch_dir = latest_version_dir.join(arch); + let Some(runtime_dir) = glob::glob(&glob_path(&arch_dir, "Microsoft.VC*.CRT"))? + .filter_map(Result::ok) + .find(|path| path.is_dir()) + else { + return Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime directory for `{arch}` in `{}`", + arch_dir.display() + ))); + }; + + Ok(runtime_dir) +} + +fn latest_vc_redist_version_dir(redist_dir: &Path) -> crate::Result> { + let dir = fs::read_dir(redist_dir)? + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .filter_map(|path| { + let version = path + .file_name()? + .to_str()? + .parse::() + .ok()?; + Some((version, path)) + }) + .max_by(|(a, _), (b, _)| a.cmp(b)) + .map(|(_, path)| path); + Ok(dir) +} + +/// Builds a glob pattern from a literal base path and an unescaped glob suffix. +/// +/// The base path is escaped so Visual Studio paths containing glob metacharacters are treated as +/// literal directories, while `pattern` remains active glob syntax. +fn glob_path(path: &Path, pattern: &str) -> String { + PathBuf::from(glob::Pattern::escape(&path.to_string_lossy())) + .join(pattern) + .to_string_lossy() + .into_owned() +} + +/// Returns the bundled `vswhere.exe` path. +/// +/// The executable is written to a temporary file so callers do not depend on a system-installed +/// `vswhere.exe`. +pub fn vswhere_path() -> Option { + let mut vswhere = std::env::temp_dir(); + vswhere.push("vswhere.exe"); + + if !vswhere.exists() { + let mut file = std::fs::File::create(&vswhere).ok()?; + file.write_all(VSWHERE).ok()?; + } + + Some(vswhere) +} + #[cfg(target_os = "windows")] -pub fn os_bitness<'a>() -> Option<&'a str> { +pub fn processor_architecture<'a>() -> Option<&'a str> { use windows_sys::Win32::System::SystemInformation::{ - GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO, + GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM, + PROCESSOR_ARCHITECTURE_ARM64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO, }; let mut system_info: SYSTEM_INFO = unsafe { std::mem::zeroed() }; @@ -74,6 +240,8 @@ pub fn os_bitness<'a>() -> Option<&'a str> { match unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture } { PROCESSOR_ARCHITECTURE_INTEL => Some("x86"), PROCESSOR_ARCHITECTURE_AMD64 => Some("x64"), + PROCESSOR_ARCHITECTURE_ARM => Some("arm"), + PROCESSOR_ARCHITECTURE_ARM64 => Some("arm64"), _ => None, } } diff --git a/crates/tauri-cli/scripts/vswhere.exe b/crates/tauri-bundler/src/bundle/windows/vswhere.exe similarity index 100% rename from crates/tauri-cli/scripts/vswhere.exe rename to crates/tauri-bundler/src/bundle/windows/vswhere.exe diff --git a/crates/tauri-bundler/src/error.rs b/crates/tauri-bundler/src/error.rs index 7794b4fa8177..b6fb181b62ce 100644 --- a/crates/tauri-bundler/src/error.rs +++ b/crates/tauri-bundler/src/error.rs @@ -79,11 +79,11 @@ pub enum Error { #[error("`{0}`")] HttpError(#[from] Box), /// Invalid glob pattern. - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[error("{0}")] GlobPattern(#[from] glob::PatternError), /// Failed to use glob pattern. - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[error("`{0}`")] Glob(#[from] glob::GlobError), /// Failed to parse the URL diff --git a/crates/tauri-cli/CHANGELOG.md b/crates/tauri-cli/CHANGELOG.md index 601cffc02c65..66b7b7d8f4ea 100644 --- a/crates/tauri-cli/CHANGELOG.md +++ b/crates/tauri-cli/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-bundler@2.9.1` + +## \[2.11.0] + +### New Features + +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. +- [`4ef5797f0`](https://www.github.com/tauri-apps/tauri/commit/4ef5797f0fb27fa2df3f39f4a54e48ef319560ec) ([#15061](https://www.github.com/tauri-apps/tauri/pull/15061)) Add `--no-sign` and `--archive-only` flags to `tauri ios build`. +- [`764b9139a`](https://www.github.com/tauri-apps/tauri/commit/764b9139a32de149d8a914a6b5ec6cd1937c64eb) ([#14313](https://www.github.com/tauri-apps/tauri/pull/14313)) Prompt to restart the Android emulator if it is not connected to adb. +- [`5dc2cee60`](https://www.github.com/tauri-apps/tauri/commit/5dc2cee60370665af88c185684432e425b1c987d) ([#14793](https://www.github.com/tauri-apps/tauri/pull/14793)) Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version` + + Notes: + + - For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION` + - For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change + +### Enhancements + +- [`be0e4bd2d`](https://www.github.com/tauri-apps/tauri/commit/be0e4bd2da02eb6cc75a8dc7c81663277e64c590) ([#15218](https://www.github.com/tauri-apps/tauri/pull/15218)) Added Vietnamese translations for the NSIS installer +- [`8718d0816`](https://www.github.com/tauri-apps/tauri/commit/8718d08163f074dfc53387ebd1d823f9c28280ee) ([#15033](https://www.github.com/tauri-apps/tauri/pull/15033)) Show the context before prompting for updater signing key password + +### Bug Fixes + +- [`fcb702ec4`](https://www.github.com/tauri-apps/tauri/commit/fcb702ec4d924e81943efaeebea8d3edb7289c33) ([#14954](https://www.github.com/tauri-apps/tauri/pull/14954)) Fix `build --bundles` to allow `nsis` arg in linux+macOS +- [`80c1425af`](https://www.github.com/tauri-apps/tauri/commit/80c1425af86058b1fc9489a30f778b6288d79b6b) ([#14921](https://www.github.com/tauri-apps/tauri/pull/14921)) Fix iOS build failure when `Metal Toolchain` is installed by using explicit `$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain` path instead of `$(TOOLCHAIN_DIR)` for Swift library search paths. + +### What's Changed + +- [`9979cde1c`](https://www.github.com/tauri-apps/tauri/commit/9979cde1c5534dafb1a07cc4dc2bc280d15d2f66) ([#15175](https://www.github.com/tauri-apps/tauri/pull/15175)) Update NSIS installer Italian translations + +### Dependencies + +- Upgraded to `tauri-macos-sign@2.3.4` +- Upgraded to `tauri-bundler@2.9.0` +- Upgraded to `tauri-utils@2.9.0` + ## \[2.10.1] ### Bug Fixes diff --git a/crates/tauri-cli/Cargo.toml b/crates/tauri-cli/Cargo.toml index 2ae2d34a4ec8..7c04af384321 100644 --- a/crates/tauri-cli/Cargo.toml +++ b/crates/tauri-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-cli" -version = "2.10.1" +version = "2.11.1" authors = ["Tauri Programme within The Commons Conservancy"] edition = "2024" rust-version = "1.88" @@ -36,7 +36,7 @@ name = "cargo-tauri" path = "src/main.rs" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies] -cargo-mobile2 = { version = "0.21.1", default-features = false } +cargo-mobile2 = { version = "0.22.4", default-features = false } [dependencies] jsonrpsee = { version = "0.24", features = ["server"] } @@ -47,7 +47,7 @@ sublime_fuzzy = "0.7" clap_complete = "4" clap = { version = "4", features = ["derive", "env"] } thiserror = "2" -tauri-bundler = { version = "2.8.1", default-features = false, path = "../tauri-bundler" } +tauri-bundler = { version = "2.9.1", default-features = false, path = "../tauri-bundler" } colored = "2" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } @@ -56,16 +56,16 @@ notify = "8" notify-debouncer-full = "0.6" shared_child = "1" duct = "1.0" -toml_edit = { version = "0.24", features = ["serde"] } +toml_edit = { version = "0.25", features = ["serde"] } json-patch = "3" -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ +tauri-utils = { version = "2.9.1", path = "../tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml", - "html-manipulation", + "html-manipulation-2", ] } -toml = "0.9" +toml = "1" jsonschema = { version = "0.33", default-features = false } handlebars = "6" include_dir = "0.7" @@ -89,8 +89,6 @@ env_logger = "0.11" icns = { package = "tauri-icns", version = "0.1" } image = { version = "0.25", default-features = false, features = ["ico"] } axum = { version = "0.8", features = ["ws"] } -html5ever = "0.29" -kuchiki = { package = "kuchikiki", version = "=0.8.8-speedreader" } tokio = { version = "1", features = ["macros", "sync"] } common-path = "1" serde-value = "0.7" @@ -105,7 +103,7 @@ oxc_span = "0.36" oxc_allocator = "0.36" oxc_ast = "0.36" magic_string = "0.3" -phf = { version = "0.11", features = ["macros"] } +phf = { version = "0.13", features = ["macros"] } walkdir = "2" elf = "0.7" memchr = "2" @@ -135,7 +133,7 @@ libc = "0.2" [target."cfg(target_os = \"macos\")".dependencies] plist = "1" -tauri-macos-sign = { version = "2.3.3", path = "../tauri-macos-sign" } +tauri-macos-sign = { version = "2.3.4", path = "../tauri-macos-sign" } object = { version = "0.36", default-features = false, features = [ "macho", "read_core", diff --git a/crates/tauri-cli/ENVIRONMENT_VARIABLES.md b/crates/tauri-cli/ENVIRONMENT_VARIABLES.md index 5a8a18100a18..f6f400ff7678 100644 --- a/crates/tauri-cli/ENVIRONMENT_VARIABLES.md +++ b/crates/tauri-cli/ENVIRONMENT_VARIABLES.md @@ -24,6 +24,8 @@ These environment variables are inputs to the CLI which may have an equivalent C - `TAURI_SIGNING_RPM_KEY` — The private GPG key used to sign the RPM bundle, exported to its ASCII-armored format. - `TAURI_SIGNING_RPM_KEY_PASSPHRASE` — The GPG key passphrase for `TAURI_SIGNING_RPM_KEY`, if needed. - `TAURI_WINDOWS_SIGNTOOL_PATH` — Specify a path to `signtool.exe` used for code signing the application on Windows. +- `STATIC_VCRUNTIME` — Deprecated. Set `build > windows > staticVCRuntime` in `tauri.conf.json` instead. +- `VCTOOLS_REDIST_DIR` — Override the Visual C++ redistributable root directory used when `bundle > windows > bundleVCRuntime` is enabled. If unset, Tauri uses its bundled `vswhere.exe` to locate Visual Studio and derive this directory. - `APPLE_CERTIFICATE` — Base64 encoded of the `.p12` certificate for code signing. To get this value, run `openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt`. - `APPLE_CERTIFICATE_PASSWORD` — The password you used to export the certificate. - `APPLE_ID` — The Apple ID used to notarize the application. If this environment variable is provided, `APPLE_PASSWORD` and `APPLE_TEAM_ID` must also be set. Alternatively, `APPLE_API_KEY` and `APPLE_API_ISSUER` can be used to authenticate. diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index cbde416db413..75c405c6c21e 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -1,5 +1,5 @@ { - "$id": "https://schema.tauri.app/config/2.10.3", + "$id": "https://schema.tauri.app/config/2.11.1", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "The Tauri configuration object.\nIt is read from a file where you can define your frontend assets,\nconfigure the bundler and define a tray icon.\n\nThe configuration file is generated by the\n[`tauri init`](https://v2.tauri.app/reference/cli/#init) command that lives in\nyour Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\nThe JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\nThe TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can\nread a platform-specific configuration from `tauri.linux.conf.json`,\n`tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n(or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\nwhich gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`app`](#appconfig): The Tauri configuration\n- [`build`](#buildconfig): The build configuration\n- [`bundle`](#bundleconfig): The bundle configurations\n- [`plugins`](#pluginconfig): The plugins configuration\n\nExample tauri.config.json file:\n\n```json\n{\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"http://localhost:3000\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n}\n```", @@ -68,7 +68,10 @@ "description": "The build configuration.", "default": { "removeUnusedCommands": false, - "additionalWatchFolders": [] + "additionalWatchFolders": [], + "windows": { + "staticVCRuntime": true + } }, "allOf": [ { @@ -94,9 +97,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "linux": { "appimage": { @@ -549,7 +554,7 @@ ] }, "backgroundThrottling": { - "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578", + "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee ", "anyOf": [ { "$ref": "#/definitions/BackgroundThrottlingPolicy" @@ -604,6 +609,32 @@ "$ref": "#/definitions/ScrollBarStyle" } ] + }, + "activityName": { + "description": "The name of the Android activity to create for this window.", + "type": [ + "string", + "null" + ] + }, + "createdByActivityName": { + "description": "The name of the Android activity that is creating this webview window.\n\nThis is important to determine which stack the activity will belong to.", + "type": [ + "string", + "null" + ] + }, + "requestedBySceneIdentifier": { + "description": "Sets the identifier of the scene that is requesting the new scene,\nestablishing a relationship between the two scenes.\n\nBy default the system uses the foreground scene.", + "type": [ + "string", + "null" + ] + }, + "generalAutofillEnabled": { + "description": "Controls the WebView's browser-level general autofill behavior.\n\n**This option does not disable password or credit card autofill.**\n\nWhen set to `false`, the WebView will not automatically populate\ngeneral form fields using previously stored data such as addresses\nor contact information.\n\nIf not specified, this is `true` by default.\n\n## Platform-specific\n\n- **Windows**: Supported. WebView2's autofill feature (called\n \"Suggestions\") may not honor `autocomplete=\"off\"` on input\n elements in some cases.\n- **Linux / Android / iOS / macOS**: Unsupported and performs no\n operation.", + "type": "boolean", + "default": true } }, "additionalProperties": false @@ -1068,7 +1099,7 @@ "const": "default" }, { - "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541", + "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee ", "type": "string", "const": "fluentOverlay" } @@ -1863,6 +1894,17 @@ "type": "string" }, "default": [] + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -1990,6 +2032,18 @@ } ] }, + "WindowsBuildConfig": { + "description": "Windows-specific build configuration.", + "type": "object", + "properties": { + "staticVCRuntime": { + "description": "Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets.", + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\nSee more: ", "type": "object", @@ -2129,9 +2183,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "allOf": [ { @@ -2370,7 +2426,7 @@ ] }, "mimeType": { - "description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.", + "description": "The mime-type of the association, e.g. `'image/png'` or `'text/plain'`.\n\n- **Linux**: written as `MimeType=` in the `.desktop` file.\n- **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of\n the `UTExportedTypeDeclarations` entry in `Info.plist`.\n- **Android**: used as `android:mimeType` in the `` element of an ``\n in `AndroidManifest.xml`.", "type": [ "string", "null" @@ -2395,6 +2451,16 @@ "type": "null" } ] + }, + "androidIntentActionFilters": { + "description": "Intent action filters for this file association.\n\nBy default all filters are used.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AndroidIntentAction" + } } }, "additionalProperties": false, @@ -2485,6 +2551,26 @@ "identifier" ] }, + "AndroidIntentAction": { + "description": "Android intent action.", + "oneOf": [ + { + "description": "ACTION_SEND.\n\n", + "type": "string", + "const": "send" + }, + { + "description": "ACTION_SEND_MULTIPLE.\n\n", + "type": "string", + "const": "sendMultiple" + }, + { + "description": "ACTION_VIEW.\n\n", + "type": "string", + "const": "view" + } + ] + }, "WindowsConfig": { "description": "Windows bundler configuration.\n\nSee more: ", "type": "object", @@ -2532,6 +2618,13 @@ "type": "boolean", "default": true }, + "minimumWebview2Version": { + "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "type": [ + "string", + "null" + ] + }, "wix": { "description": "Configuration for the MSI generated with WiX.", "anyOf": [ @@ -2564,6 +2657,11 @@ "type": "null" } ] + }, + "bundleVCRuntime": { + "description": "Whether to bundle the Visual C++ runtime DLLs alongside the application.\n\nThis can be particularly useful when your application includes sidecars or DLLs that do\nnot statically link the Visual C++ runtime and require the runtime DLLs at runtime, and\nyou do not want to require users to install the Visual C++ Redistributable. This can also\nbe useful when `build > windows > staticVCRuntime` is set to `false`.", + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -2842,6 +2940,20 @@ "null" ] }, + "uninstallerIcon": { + "description": "The path to an icon file used as the uninstaller icon.", + "type": [ + "string", + "null" + ] + }, + "uninstallerHeaderImage": { + "description": "The path to a bitmap file to display on the header of uninstallers pages.\nDefaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`\n\nThe recommended dimensions are 150px x 57px.", + "type": [ + "string", + "null" + ] + }, "installMode": { "description": "Whether the installation will be for all users or just the current user.", "default": "currentUser", @@ -2852,7 +2964,7 @@ ] }, "languages": { - "description": "A list of installer languages.\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", + "description": "A list of installer languages. Default to `[\"English\"]` if not set.\n\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", "type": [ "array", "null" @@ -2862,7 +2974,7 @@ } }, "customLanguageFiles": { - "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,", + "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array,", "type": [ "object", "null" @@ -2900,11 +3012,12 @@ ] }, "minimumWebview2Version": { - "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "description": "Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead.\n\nTry to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", "type": [ "string", "null" - ] + ], + "deprecated": true } }, "additionalProperties": false @@ -3685,6 +3798,13 @@ "description": "Whether to automatically increment the `versionCode` on each build.\n\n- If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n- If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\nNote that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.", "type": "boolean", "default": false + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\nThis allows installing debug and release versions side-by-side on the same device.\nExample: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/metadata-v2.json b/crates/tauri-cli/metadata-v2.json index 3e034233271e..091d891cd366 100644 --- a/crates/tauri-cli/metadata-v2.json +++ b/crates/tauri-cli/metadata-v2.json @@ -1,9 +1,9 @@ { "cli.js": { - "version": "2.10.1", + "version": "2.11.1", "node": ">= 10.0.0" }, - "tauri": "2.10.3", - "tauri-build": "2.5.6", - "tauri-plugin": "2.5.4" + "tauri": "2.11.1", + "tauri-build": "2.6.1", + "tauri-plugin": "2.6.1" } diff --git a/crates/tauri-cli/schema.json b/crates/tauri-cli/schema.json index 6bd2b3bc87bf..903f05633e63 100644 --- a/crates/tauri-cli/schema.json +++ b/crates/tauri-cli/schema.json @@ -3095,6 +3095,13 @@ "format": "uint32", "maximum": 2100000000.0, "minimum": 1.0 + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/src/bundle.rs b/crates/tauri-cli/src/bundle.rs index b09baba1e7b8..a12234056cad 100644 --- a/crates/tauri-cli/src/bundle.rs +++ b/crates/tauri-cli/src/bundle.rs @@ -43,7 +43,7 @@ impl ValueEnum for BundleFormat { } fn to_possible_value(&self) -> Option { - let hide = self.0 == PackageType::Updater; + let hide = (!cfg!(windows) && self.0 == PackageType::Nsis) || self.0 == PackageType::Updater; Some(PossibleValue::new(self.0.short_name()).hide(hide)) } } diff --git a/crates/tauri-cli/src/dev/builtin_dev_server.rs b/crates/tauri-cli/src/dev/builtin_dev_server.rs index b99c358197de..6b2d513d8db0 100644 --- a/crates/tauri-cli/src/dev/builtin_dev_server.rs +++ b/crates/tauri-cli/src/dev/builtin_dev_server.rs @@ -7,8 +7,6 @@ use axum::{ http::{StatusCode, Uri, header}, response::{IntoResponse, Response}, }; -use html5ever::{LocalName, QualName, namespace_url, ns}; -use kuchiki::{NodeRef, traits::TendrilSink}; use std::{ net::{IpAddr, SocketAddr}, path::{Path, PathBuf}, @@ -128,30 +126,14 @@ async fn ws_handler(ws: WebSocketUpgrade, state: State) -> Response } fn inject_address(html_bytes: Vec, address: &SocketAddr) -> Vec { - fn with_html_head(document: &mut NodeRef, f: F) { - if let Ok(ref node) = document.select_first("head") { - f(node.as_node()) - } else { - let node = NodeRef::new_element( - QualName::new(None, ns!(html), LocalName::from("head")), - None, - ); - f(&node); - document.prepend(node) - } - } + let document = tauri_utils::html2::parse_doc(String::from_utf8_lossy(&html_bytes).into_owned()); - let mut document = kuchiki::parse_html() - .one(String::from_utf8_lossy(&html_bytes).into_owned()) - .document_node; - with_html_head(&mut document, |head| { - let script = RELOAD_SCRIPT.replace("{{reload_url}}", &format!("ws://{address}/__tauri_cli")); - let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); - script_el.append(NodeRef::new_text(script)); - head.prepend(script_el); - }); + tauri_utils::html2::append_script_to_head( + &document, + &RELOAD_SCRIPT.replace("{{reload_url}}", &format!("ws://{address}/__tauri_cli")), + ); - tauri_utils::html::serialize_node(&document) + tauri_utils::html2::serialize_doc(&document) } fn fs_read_scoped(path: PathBuf, scope: &Path) -> crate::Result> { diff --git a/crates/tauri-cli/src/helpers/config.rs b/crates/tauri-cli/src/helpers/config.rs index dccedac8414c..b20b929cb666 100644 --- a/crates/tauri-cli/src/helpers/config.rs +++ b/crates/tauri-cli/src/helpers/config.rs @@ -109,6 +109,8 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings { header_image: config.header_image, sidebar_image: config.sidebar_image, installer_icon: config.installer_icon, + uninstaller_icon: config.uninstaller_icon, + uninstaller_header_image: config.uninstaller_header_image, install_mode: config.install_mode, languages: config.languages, custom_language_files: config.custom_language_files, @@ -116,6 +118,7 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings { compression: config.compression, start_menu_folder: config.start_menu_folder, installer_hooks: config.installer_hooks, + #[allow(deprecated)] minimum_webview2_version: config.minimum_webview2_version, } } diff --git a/crates/tauri-cli/src/info/env_system.rs b/crates/tauri-cli/src/info/env_system.rs index 0087319eb820..fb429c0db8f5 100644 --- a/crates/tauri-cli/src/info/env_system.rs +++ b/crates/tauri-cli/src/info/env_system.rs @@ -17,20 +17,10 @@ struct VsInstanceInfo { display_name: String, } -#[cfg(windows)] -const VSWHERE: &[u8] = include_bytes!("../../scripts/vswhere.exe"); - #[cfg(windows)] fn build_tools_version() -> crate::Result> { - let mut vswhere = std::env::temp_dir(); - vswhere.push("vswhere.exe"); - - if !vswhere.exists() { - if let Ok(mut file) = std::fs::File::create(&vswhere) { - use std::io::Write; - let _ = file.write_all(VSWHERE); - } - } + let vswhere = + tauri_bundler::bundle::vswhere_path().context("failed to find or prepare vswhere.exe")?; // Check if there are Visual Studio installations that have the "MSVC - C++ Buildtools" and "Windows SDK" components. // Both the Windows 10 and Windows 11 SDKs work so we need to query it twice. diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 14474900e612..2834c7a109a0 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -1710,6 +1710,8 @@ pub(crate) fn tauri_config_to_bundle_settings( webview_install_mode: config.windows.webview_install_mode, allow_downgrades: config.windows.allow_downgrades, sign_command: config.windows.sign_command.map(custom_sign_settings), + minimum_webview2_version: config.windows.minimum_webview2_version, + bundle_vc_runtime: config.windows.bundle_vc_runtime, }, license: config.license.or_else(|| { settings diff --git a/crates/tauri-cli/src/interface/rust/desktop.rs b/crates/tauri-cli/src/interface/rust/desktop.rs index 7ccfa85e4353..d21a7910ca96 100644 --- a/crates/tauri-cli/src/interface/rust/desktop.rs +++ b/crates/tauri-cli/src/interface/rust/desktop.rs @@ -194,10 +194,6 @@ pub fn build( let out_dir = app_settings.out_dir(&options, tauri_dir)?; let bin_path = app_settings.app_binary_path(&options, tauri_dir)?; - if std::env::var_os("STATIC_VCRUNTIME").is_none_or(|v| v != "false") { - unsafe { std::env::set_var("STATIC_VCRUNTIME", "true") }; - } - if options.target == Some("universal-apple-darwin".into()) { std::fs::create_dir_all(&out_dir) .fs_context("failed to create project out directory", out_dir.clone())?; diff --git a/crates/tauri-cli/src/mobile/android/android_studio_script.rs b/crates/tauri-cli/src/mobile/android/android_studio_script.rs index 189d4f8fd777..f007bb78a64b 100644 --- a/crates/tauri-cli/src/mobile/android/android_studio_script.rs +++ b/crates/tauri-cli/src/mobile/android/android_studio_script.rs @@ -13,7 +13,7 @@ use crate::{ use clap::{ArgAction, Parser}; use cargo_mobile2::{ - android::{adb, target::Target}, + android::{adb, device::ConnectionStatus, target::Target}, opts::Profile, target::{TargetTrait, call_for_targets_with_fallback}, }; @@ -192,7 +192,11 @@ fn adb_forward_port( let forward = format!("tcp:{port}"); log::info!("Forwarding port {port} with adb"); - let mut devices = adb::device_list(env).unwrap_or_default(); + let mut devices = adb::device_list(env) + .unwrap_or_default() + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); // if we could not detect any running device, let's wait a few seconds, it might be booting up if devices.is_empty() { log::warn!( @@ -204,7 +208,11 @@ fn adb_forward_port( loop { std::thread::sleep(std::time::Duration::from_secs(1)); - devices = adb::device_list(env).unwrap_or_default(); + devices = adb::device_list(env) + .unwrap_or_default() + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); if !devices.is_empty() { break; } diff --git a/crates/tauri-cli/src/mobile/android/build.rs b/crates/tauri-cli/src/mobile/android/build.rs index b0e3ce615933..030ec8eb7775 100644 --- a/crates/tauri-cli/src/mobile/android/build.rs +++ b/crates/tauri-cli/src/mobile/android/build.rs @@ -4,7 +4,7 @@ use super::{ MobileTarget, OptionsHandle, configure_cargo, delete_codegen_vars, ensure_init, env, get_app, - get_config, inject_resources, log_finished, open_and_wait, + get_config, inject_resources, log_finished, open_and_wait, sync_debug_application_id_suffix, }; use crate::{ ConfigValue, Error, Result, @@ -192,6 +192,7 @@ pub fn run( configure_cargo(&mut env, &config)?; generate_tauri_properties(&config, tauri_config, false)?; + sync_debug_application_id_suffix(&config, tauri_config)?; crate::build::setup(&interface, &mut build_options, tauri_config, dirs, true)?; diff --git a/crates/tauri-cli/src/mobile/android/dev.rs b/crates/tauri-cli/src/mobile/android/dev.rs index 3799063e1d23..7c2c6c6bf9ff 100644 --- a/crates/tauri-cli/src/mobile/android/dev.rs +++ b/crates/tauri-cli/src/mobile/android/dev.rs @@ -4,7 +4,7 @@ use super::{ MobileTarget, configure_cargo, delete_codegen_vars, device_prompt, ensure_init, env, get_app, - get_config, inject_resources, open_and_wait, + get_config, inject_resources, open_and_wait, sync_debug_application_id_suffix, }; use crate::{ ConfigValue, Error, Result, @@ -276,6 +276,7 @@ fn run_dev( configure_cargo(&mut env, config)?; generate_tauri_properties(config, &tauri_config, true)?; + sync_debug_application_id_suffix(config, &tauri_config)?; let installed_targets = crate::interface::rust::installation::installed_targets().unwrap_or_default(); @@ -341,7 +342,15 @@ fn run_dev( if open { open_and_wait(config, &env) } else if let Some(device) = &device { - match run(device, options, config, &env, metadata, noise_level) { + match run( + device, + options, + config, + &env, + metadata, + noise_level, + tauri_config, + ) { Ok(c) => Ok(Box::new(c) as Box), Err(e) => { crate::dev::kill_before_dev_process(); @@ -363,6 +372,7 @@ fn run( env: &Env, metadata: &AndroidMetadata, noise_level: NoiseLevel, + tauri_config: &tauri_utils::config::Config, ) -> crate::Result { let profile = if options.debug { Profile::Debug @@ -372,8 +382,18 @@ fn run( let build_app_bundle = metadata.asset_packs().is_some(); + let application_id_suffix = if profile == Profile::Debug { + tauri_config + .bundle + .android + .debug_application_id_suffix + .clone() + } else { + None + }; + device - .run( + .run_with_application_id_suffix( config, env, noise_level, @@ -385,7 +405,8 @@ fn run( }), build_app_bundle, false, - ".MainActivity".into(), + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, ) .map(DevChild::new) .context("failed to run Android app") diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index a372a25f488f..2c29ce2336bf 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -6,7 +6,7 @@ use cargo_mobile2::{ android::{ adb, config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig}, - device::Device, + device::{ConnectionStatus, Device}, emulator, env::Env, target::Target, @@ -25,6 +25,7 @@ use std::{ io::Cursor, path::{Path, PathBuf}, process::{Command, exit}, + sync::OnceLock, thread::sleep, time::Duration, }; @@ -192,6 +193,247 @@ pub fn get_config( (config, metadata) } +fn sync_debug_application_id_suffix( + config: &AndroidConfig, + tauri_config: &TauriConfig, +) -> Result<()> { + let build_gradle_path = config.project_dir().join("app").join("build.gradle.kts"); + let build_gradle = std::fs::read_to_string(&build_gradle_path).fs_context( + "failed to read Android Gradle build file", + build_gradle_path.clone(), + )?; + let Some(updated_build_gradle) = set_debug_application_id_suffix( + &build_gradle, + tauri_config + .bundle + .android + .debug_application_id_suffix + .as_deref(), + ) else { + crate::error::bail!( + "Could not find the Android debug build type in {}. Add a `getByName(\"debug\")` build type or run `tauri android init` to regenerate the Android project.", + build_gradle_path.display() + ); + }; + + if updated_build_gradle != build_gradle { + write(&build_gradle_path, updated_build_gradle).fs_context( + "failed to write Android Gradle build file", + build_gradle_path, + )?; + } + + Ok(()) +} + +fn set_debug_application_id_suffix(build_gradle: &str, suffix: Option<&str>) -> Option { + static DEBUG_BUILD_TYPE_RE: OnceLock = OnceLock::new(); + + let debug_build_type_re = DEBUG_BUILD_TYPE_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)(?:\bgetByName\(\s*"debug"\s*\)|\bdebug\b)\s*\{"#) + .expect("valid debug build type regex") + }); + + for build_type_match in debug_build_type_re.find_iter(build_gradle) { + let Some(opening_brace) = build_gradle[build_type_match.start()..] + .find('{') + .map(|index| build_type_match.start() + index) + else { + continue; + }; + let Some(closing_brace) = find_matching_brace(build_gradle, opening_brace) else { + continue; + }; + + let debug_block = &build_gradle[opening_brace..closing_brace]; + let updated_debug_block = set_application_id_suffix_in_block(debug_block, suffix); + let mut updated_build_gradle = + String::with_capacity(build_gradle.len() + updated_debug_block.len()); + updated_build_gradle.push_str(&build_gradle[..opening_brace]); + updated_build_gradle.push_str(&updated_debug_block); + updated_build_gradle.push_str(&build_gradle[closing_brace..]); + return Some(updated_build_gradle); + } + + None +} + +fn set_application_id_suffix_in_block(debug_block: &str, suffix: Option<&str>) -> String { + static APPLICATION_ID_SUFFIX_RE: OnceLock = OnceLock::new(); + + let application_id_suffix_re = APPLICATION_ID_SUFFIX_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)^[ \t]*applicationIdSuffix\s*=.*(?:\r?\n)?"#) + .expect("valid applicationIdSuffix regex") + }); + + if let Some(application_id_suffix_match) = application_id_suffix_re.find(debug_block) { + let mut updated_debug_block = String::with_capacity(debug_block.len()); + updated_debug_block.push_str(&debug_block[..application_id_suffix_match.start()]); + if let Some(suffix) = suffix { + let indentation = debug_block + [application_id_suffix_match.start()..application_id_suffix_match.end()] + .chars() + .take_while(|character| *character == ' ' || *character == '\t') + .collect::(); + updated_debug_block.push_str(&format!( + "{indentation}applicationIdSuffix = \"{}\"\n", + escape_kotlin_string(suffix) + )); + } + updated_debug_block.push_str(&debug_block[application_id_suffix_match.end()..]); + return updated_debug_block; + } + + let Some(suffix) = suffix else { + return debug_block.to_string(); + }; + + let indentation = debug_block_indentation(debug_block); + let application_id_suffix = format!( + "{indentation}applicationIdSuffix = \"{}\"\n", + escape_kotlin_string(suffix) + ); + + if let Some(first_newline) = debug_block.find('\n') { + let mut updated_debug_block = + String::with_capacity(debug_block.len() + application_id_suffix.len()); + updated_debug_block.push_str(&debug_block[..=first_newline]); + updated_debug_block.push_str(&application_id_suffix); + updated_debug_block.push_str(&debug_block[first_newline + 1..]); + updated_debug_block + } else { + format!("{{\n{application_id_suffix}") + } +} + +fn debug_block_indentation(debug_block: &str) -> &str { + debug_block + .lines() + .skip(1) + .find_map(|line| { + if line.trim().is_empty() { + None + } else { + Some(line.trim_end().trim_end_matches(line.trim_start())) + } + }) + .unwrap_or(" ") +} + +fn find_matching_brace(content: &str, opening_brace: usize) -> Option { + let mut depth = 0u32; + let mut in_line_comment = false; + let mut in_block_comment = false; + let mut in_string = false; + let mut in_raw_string = false; + let mut string_quote = '\0'; + let mut escaped = false; + let mut previous = '\0'; + let mut chars = content[opening_brace..].char_indices().peekable(); + + while let Some((relative_index, character)) = chars.next() { + let index = opening_brace + relative_index; + + if in_line_comment { + if character == '\n' { + in_line_comment = false; + } + previous = character; + continue; + } + + if in_block_comment { + if previous == '*' && character == '/' { + in_block_comment = false; + } + previous = character; + continue; + } + + if in_raw_string { + if content[index..].starts_with("\"\"\"") { + // Consume the remaining two quotes in the Kotlin raw string delimiter. + let _ = chars.next(); + let _ = chars.next(); + in_raw_string = false; + } + previous = character; + continue; + } + + if in_string { + if escaped { + escaped = false; + } else if character == '\\' { + escaped = true; + } else if character == string_quote { + in_string = false; + } + previous = character; + continue; + } + + if character == '/' && chars.peek().is_some_and(|(_, next)| *next == '/') { + in_line_comment = true; + previous = character; + continue; + } + + if character == '/' && chars.peek().is_some_and(|(_, next)| *next == '*') { + in_block_comment = true; + previous = character; + continue; + } + + if content[index..].starts_with("\"\"\"") { + // Consume the remaining two quotes in the Kotlin raw string delimiter. + let _ = chars.next(); + let _ = chars.next(); + in_raw_string = true; + previous = character; + continue; + } + + if character == '"' || character == '\'' { + in_string = true; + string_quote = character; + previous = character; + continue; + } + + if character == '{' { + depth = depth.saturating_add(1); + } else if character == '}' { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(index); + } + } + + previous = character; + } + + None +} + +fn escape_kotlin_string(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '$' => output.push_str("\\$"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + other => output.push(other), + } + } + + output +} + pub fn env(non_interactive: bool) -> Result { let env = super::env().context("failed to setup Android environment")?; ensure_env(non_interactive).context("failed to ensure Android environment")?; @@ -457,7 +699,11 @@ fn delete_codegen_vars() { } fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { - let device_list = adb::device_list(env).context("failed to detect connected Android devices")?; + let device_list = adb::device_list(env) + .context("failed to detect connected Android devices")? + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); if !device_list.is_empty() { let device = if let Some(t) = target { let (device, score) = device_list @@ -543,31 +789,174 @@ fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result(env: &'_ Env, target: Option<&str>) -> Result> { if let Ok(device) = adb_device_prompt(env, target) { Ok(device) } else { let emulator = emulator_prompt(env, target)?; - log::info!("Starting emulator {}", emulator.name()); - emulator - .start_detached(env) - .context("failed to start emulator")?; + let emulator_status = match adb::device_list(env) { + Ok(devices) => { + // emulator might be running but disconnected from adb + devices + .iter() + .find(|d| d.name() == emulator.name()) + .and_then(|d| match d.status() { + ConnectionStatus::Offline | ConnectionStatus::Unauthorized => { + Some(EmulatorStatus::Offline { + serial_no: d.serial_no().to_string(), + }) + } + ConnectionStatus::Connected => Some(EmulatorStatus::Connected), + _ => None, + }) + } + // failed to get device information, check if the device name matches the emulator name + Err( + adb::device_list::Error::ModelFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + } + | adb::device_list::Error::AbiFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + }, + ) => { + if error.kind() == std::io::ErrorKind::TimedOut { + // if the device name matches the emulator name, the emulator is already running and marked as connected + // but we cannot connect to it + adb::device_name(env, &serial_no).map_or(None, |device_name| { + if device_name == emulator.name() { + Some(EmulatorStatus::Offline { serial_no }) + } else { + None + } + }) + } else { + None + } + } + Err(_) => None, + }; + + let emulator_already_running = emulator_status.is_some(); + match emulator_status { + Some(EmulatorStatus::Offline { serial_no }) => { + // emulator is available but not connected to adb, we must restart it + log::info!("Emulator is not connected, we need to restart it"); + restart_emulator(env, &serial_no, &emulator)?; + } + Some(EmulatorStatus::Connected) => { + // emulator is already connected to adb + // this is technically unreachable because we queried the device list with adb_device_prompt + } + None => { + log::info!("Starting emulator {}", emulator.name()); + emulator + .start_detached(env) + .context("failed to start emulator")?; + } + } + let mut tries = 0; loop { sleep(Duration::from_secs(2)); - if let Ok(device) = adb_device_prompt(env, Some(emulator.name())) { - return Ok(device); + // we do not filter for connected devices to detect emulators that are not connected to our adb anymore + match adb::device_list(env) { + Ok(devices) => { + if let Some(device) = devices.into_iter().find(|d| d.name() == emulator.name()) + && device.status() == ConnectionStatus::Connected + { + return Ok(device); + } + + if tries >= 3 { + log::info!( + "Waiting for emulator to start... (maybe the emulator is unauthorized or offline, run `adb devices` to check)" + ); + } else { + log::info!("Waiting for emulator to start..."); + } + tries += 1; + } + Err( + adb::device_list::Error::ModelFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + } + | adb::device_list::Error::AbiFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + }, + ) => { + if emulator_already_running && error.kind() == std::io::ErrorKind::TimedOut { + log::info!("Emulator is not responding, we need to restart it"); + restart_emulator(env, &serial_no, &emulator)?; + tries = 0; + } else { + log::error!("failed to get properties for device {serial_no}: {error}"); + } + } + Err(e) => { + log::error!("failed to list devices with adb: {e}"); + tries += 1; + } } - if tries >= 3 { - log::info!( - "Waiting for emulator to start... (maybe the emulator is unauthorized or offline, run `adb devices` to check)" - ); - } else { - log::info!("Waiting for emulator to start..."); + } + } +} + +fn restart_emulator(env: &Env, serial_no: &str, emulator: &emulator::Emulator) -> Result<()> { + let granted_permission_to_restart = + crate::helpers::prompts::confirm("Do you want to restart the emulator?", Some(true)) + .unwrap_or_default(); + if !granted_permission_to_restart { + crate::error::bail!( + "Cannot connect to the emulator, please restart it manually (a full boot might be required)" + ); + } + + adb::adb(env, &["-s", serial_no, "emu", "kill"]) + .run() + .context("failed to reboot emulator")?; + + log::info!("Waiting for emulator to exit..."); + loop { + let devices = adb::device_list(env).unwrap_or_default(); + if devices + .into_iter() + .find(|d| d.serial_no() == serial_no) + .is_none() + { + break; + } + sleep(Duration::from_secs(1)); + } + + log::info!("Restarting emulator with full boot"); + let mut tries = 0; + loop { + // wait a bit to make sure we can restart the emulator + sleep(Duration::from_secs(2)); + + match emulator.start_detached_with_options(env, emulator::StartOptions::new().full_boot()) { + Ok(_) => break, + Err(e) => { + tries += 1; + if tries >= 3 { + return Err(e).context("failed to start emulator"); + } else { + log::error!("failed to start emulator, retrying..."); + } } - tries += 1; } } + + Ok(()) } fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { @@ -699,3 +1088,129 @@ fn generate_tauri_properties( Ok(()) } + +#[cfg(test)] +mod tests { + use super::{find_matching_brace, set_debug_application_id_suffix}; + + #[test] + fn writes_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".debug")).unwrap(); + + assert!(updated.contains( + r#" getByName("debug") { + applicationIdSuffix = ".debug" + manifestPlaceholders["usesCleartextTraffic"] = "true""# + )); + } + + #[test] + fn replaces_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + applicationIdSuffix = ".old" + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".internal")).unwrap(); + + assert!(updated.contains(r#" applicationIdSuffix = ".internal""#)); + assert!(!updated.contains(r#".old"#)); + } + + #[test] + fn removes_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + applicationIdSuffix = ".debug" + } + getByName("release") { + applicationIdSuffix = ".release" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, None).unwrap(); + + assert!(!updated.contains(r#"applicationIdSuffix = ".debug""#)); + assert!(updated.contains(r#"applicationIdSuffix = ".release""#)); + } + + #[test] + fn writes_debug_suffix_before_nested_blocks() { + let build_gradle = r#" +android { + buildTypes { + debug { + packaging { + jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + } + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".internal")).unwrap(); + + assert!(updated.contains( + r#" debug { + applicationIdSuffix = ".internal" + packaging {"# + )); + } + + #[test] + fn ignores_braces_inside_kotlin_raw_strings() { + let build_gradle = r#" +android { + buildTypes { + debug { + val proguardRules = """ + -if class ** { + public *; + } + """ + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let opening_brace = build_gradle + .find("debug {") + .and_then(|index| build_gradle[index..].find('{').map(|brace| index + brace)) + .unwrap(); + let closing_brace = find_matching_brace(build_gradle, opening_brace).unwrap(); + + assert!( + build_gradle[opening_brace..closing_brace] + .contains(r#"manifestPlaceholders["usesCleartextTraffic"] = "true""#) + ); + + let updated = set_debug_application_id_suffix(build_gradle, Some(".debug")).unwrap(); + + assert!(updated.contains( + r#" debug { + applicationIdSuffix = ".debug" + val proguardRules = """"# + )); + } +} diff --git a/crates/tauri-cli/src/mobile/android/run.rs b/crates/tauri-cli/src/mobile/android/run.rs index 545be0025217..726d62ef8b45 100644 --- a/crates/tauri-cli/src/mobile/android/run.rs +++ b/crates/tauri-cli/src/mobile/android/run.rs @@ -10,7 +10,7 @@ use cargo_mobile2::{ use clap::{ArgAction, Parser}; use std::path::PathBuf; -use super::{configure_cargo, device_prompt, env}; +use super::{configure_cargo, device_prompt, env, sync_debug_application_id_suffix}; use crate::{ ConfigValue, Result, error::Context, @@ -127,9 +127,22 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { if let Some(device) = device { let config = built_application.config.clone(); let release = options.release; - let runner = move |_tauri_config: &ConfigMetadata| { + + let runner = move |tauri_config: &ConfigMetadata| { + sync_debug_application_id_suffix(&config, tauri_config)?; + + let application_id_suffix = if !release { + tauri_config + .bundle + .android + .debug_application_id_suffix + .clone() + } else { + None + }; + device - .run( + .run_with_application_id_suffix( &config, &env, noise_level, @@ -145,7 +158,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { }), false, false, - ".MainActivity".into(), + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, ) .map(|c| Box::new(DevChild::new(c)) as Box) .context("failed to run Android app") diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs index 48389dbea8fc..50f8babcc65b 100644 --- a/crates/tauri-cli/src/mobile/init.rs +++ b/crates/tauri-cli/src/mobile/init.rs @@ -140,6 +140,13 @@ fn exec( let (config, metadata) = super::android::get_config(&app, &tauri_config, &[], &Default::default()); map.insert("android", &config); + + // Add application_id_suffix to the map for template access + // The template will access it via a helper or we'll modify template to use root context + if let Some(suffix) = &tauri_config.bundle.android.debug_application_id_suffix { + map.insert("android-debug-application-id-suffix", suffix); + } + super::android::project::generate( &config, &metadata, diff --git a/crates/tauri-cli/src/mobile/ios/build.rs b/crates/tauri-cli/src/mobile/ios/build.rs index 8242170350d5..06dc396cd8db 100644 --- a/crates/tauri-cli/src/mobile/ios/build.rs +++ b/crates/tauri-cli/src/mobile/ios/build.rs @@ -36,7 +36,7 @@ use rand::distr::{Alphanumeric, SampleString}; use std::{ env::{set_current_dir, var, var_os}, fs, - path::PathBuf, + path::{Path, PathBuf}, }; #[derive(Debug, Clone, Parser)] @@ -94,6 +94,12 @@ pub struct Options { /// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior. #[clap(long)] pub ignore_version_mismatches: bool, + /// Skip code signing when bundling the app + #[clap(long)] + pub no_sign: bool, + /// Only archive the app, skip generating the IPA. + #[clap(long)] + pub archive_only: bool, /// Target device of this build #[clap(skip)] pub target_device: Option, @@ -154,7 +160,7 @@ impl From for BuildOptions { ci: options.ci, skip_stapling: false, ignore_version_mismatches: options.ignore_version_mismatches, - no_sign: false, + no_sign: options.no_sign, } } } @@ -243,8 +249,15 @@ pub fn run(options: Options, noise_level: NoiseLevel, dirs: &Dirs) -> Result Result<()> { + let ipa_file = + fs::File::create(ipa_path).fs_context("failed to create IPA file", ipa_path.to_path_buf())?; + let mut zip = zip::ZipWriter::new(ipa_file); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .unix_permissions(0o755); + + zip + .add_directory("Payload/", options) + .context("failed to add Payload directory to zip")?; + + let mut app_files = Vec::new(); + let mut stack = vec![app_path.to_path_buf()]; + while let Some(path) = stack.pop() { + if path.is_dir() { + app_files.push(path.clone()); + for entry in fs::read_dir(&path).fs_context("failed to read directory", path.clone())? { + stack.push( + entry + .fs_context("failed to read directory entry", path.clone())? + .path(), + ); + } + } else { + app_files.push(path); + } + } + + for file_path in app_files { + let name = file_path.strip_prefix(app_path.parent().unwrap()).unwrap(); + let mut name_str = name.to_string_lossy().to_string(); + // zip expects forward slashes + if std::path::MAIN_SEPARATOR == '\\' { + name_str = name_str.replace('\\', "/"); + } + let mut name_in_zip = format!("Payload/{name_str}"); + + if file_path.is_dir() { + name_in_zip.push('/'); + zip + .add_directory(name_in_zip, options) + .context("failed to add directory to zip")?; + } else { + zip + .start_file(name_in_zip, options) + .context("failed to start file in zip")?; + let mut f = fs::File::open(&file_path).fs_context("failed to open file", file_path)?; + std::io::copy(&mut f, &mut zip).context("failed to copy file to zip")?; + } + } + + zip.finish().context("failed to finish zip")?; + Ok(()) +} + fn auth_credentials_from_env() -> Result> { match ( var("APPLE_API_KEY"), diff --git a/crates/tauri-cli/src/mobile/ios/dev.rs b/crates/tauri-cli/src/mobile/ios/dev.rs index 99f56c5ee115..8c897ec2557b 100644 --- a/crates/tauri-cli/src/mobile/ios/dev.rs +++ b/crates/tauri-cli/src/mobile/ios/dev.rs @@ -232,8 +232,15 @@ fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result< if dirs.tauri.join("Info.ios.plist").exists() { src_plists.push(dirs.tauri.join("Info.ios.plist").into()); } - if let Some(info_plist) = &tauri_config.bundle.ios.info_plist { - src_plists.push(info_plist.clone().into()); + { + if let Some(info_plist) = &tauri_config.bundle.ios.info_plist { + src_plists.push(info_plist.clone().into()); + } + if let Some(associations) = tauri_config.bundle.file_associations.as_ref() + && let Some(file_associations) = tauri_utils::config::file_associations_plist(associations) + { + src_plists.push(file_associations.into()); + } } let merged_info_plist = merge_plist(src_plists)?; merged_info_plist diff --git a/crates/tauri-cli/src/mobile/ios/run.rs b/crates/tauri-cli/src/mobile/ios/run.rs index bf712ed62768..a40da843764d 100644 --- a/crates/tauri-cli/src/mobile/ios/run.rs +++ b/crates/tauri-cli/src/mobile/ios/run.rs @@ -88,6 +88,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { export_method: None, args: options.args, ignore_version_mismatches: options.ignore_version_mismatches, + no_sign: false, + archive_only: false, target_device: device.as_ref().map(|d| TargetDevice { id: d.id().to_string(), name: d.name().to_string(), diff --git a/crates/tauri-cli/tauri.config.schema.json b/crates/tauri-cli/tauri.config.schema.json index b6c64b09b99b..ea575a22234f 100644 --- a/crates/tauri-cli/tauri.config.schema.json +++ b/crates/tauri-cli/tauri.config.schema.json @@ -3081,7 +3081,7 @@ "additionalProperties": false }, "AndroidConfig": { - "description": "General configuration for the iOS target.", + "description": "General configuration for the Android target.", "type": "object", "properties": { "minSdkVersion": { @@ -3100,6 +3100,13 @@ "format": "uint32", "maximum": 2100000000.0, "minimum": 1.0 + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/templates/mobile/android/.gitignore b/crates/tauri-cli/templates/mobile/android/.gitignore index b24820317285..1c636c39a750 100644 --- a/crates/tauri-cli/templates/mobile/android/.gitignore +++ b/crates/tauri-cli/templates/mobile/android/.gitignore @@ -14,6 +14,7 @@ build .cxx local.properties key.properties +keystore.properties /.tauri /tauri.settings.gradle \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts b/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts index 621f7bcaeabe..0f73195fa008 100644 --- a/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts +++ b/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts @@ -28,6 +28,9 @@ android { } buildTypes { getByName("debug") { + {{#if android-debug-application-id-suffix}} + applicationIdSuffix = "{{android-debug-application-id-suffix}}" + {{/if}} manifestPlaceholders["usesCleartextTraffic"] = "true" isDebuggable = true isJniDebuggable = true @@ -68,6 +71,7 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.activity:activity-ktx:1.10.1") implementation("com.google.android.material:material:1.12.0") + implementation("androidx.lifecycle:lifecycle-process:2.10.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.4") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") diff --git a/crates/tauri-cli/templates/mobile/ios/project.yml b/crates/tauri-cli/templates/mobile/ios/project.yml index 1994d5e5aff7..c23c1949e33d 100644 --- a/crates/tauri-cli/templates/mobile/ios/project.yml +++ b/crates/tauri-cli/templates/mobile/ios/project.yml @@ -78,8 +78,8 @@ targets: ENABLE_BITCODE: false ARCHS: [{{join ios-valid-archs}}] VALID_ARCHS: {{~#each ios-valid-archs}} {{this}} {{/each}} - LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) - LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 groups: [app] diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json index 6217d62bd8e5..c660a10807c1 100644 --- a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json @@ -14,9 +14,9 @@ "tauri-plugin-{{ plugin_name }}-api": "file:../../" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^6.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.0.0", - "vite": "^7.0.0", + "vite": "^8.0.0", "@tauri-apps/cli": "^2.0.0" } } diff --git a/crates/tauri-cli/templates/plugin/package.json b/crates/tauri-cli/templates/plugin/package.json index 1dd60abc81e3..3235e3fd18f9 100644 --- a/crates/tauri-cli/templates/plugin/package.json +++ b/crates/tauri-cli/templates/plugin/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@rollup/plugin-typescript": "^12.0.0", "rollup": "^4.9.6", - "typescript": "^5.3.3", + "typescript": "^6.0.0", "tslib": "^2.6.2" } } diff --git a/crates/tauri-cli/templates/tauri.conf.json b/crates/tauri-cli/templates/tauri.conf.json index 6b8354f78464..048fc0f00d8c 100644 --- a/crates/tauri-cli/templates/tauri.conf.json +++ b/crates/tauri-cli/templates/tauri.conf.json @@ -32,6 +32,9 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "android": { + "debugApplicationIdSuffix": ".debug" + } } } diff --git a/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj b/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj index a3c74da6b00d..2604ffdc40a5 100644 --- a/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj +++ b/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj @@ -398,8 +398,8 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; @@ -430,8 +430,8 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; diff --git a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap index a3dc6bf5ed43..97ba4c6235c9 100644 --- a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap +++ b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap @@ -402,8 +402,8 @@ expression: pbxproj.serialize() "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; @@ -434,8 +434,8 @@ expression: pbxproj.serialize() "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri Test"; SDKROOT = iphoneos; diff --git a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap index d84a8c0e71ba..24c4db8b9f99 100644 --- a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap +++ b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap @@ -708,13 +708,13 @@ Pbxproj { identation: "\t\t\t\t", line_number: 400, key: "\"LIBRARY_SEARCH_PATHS[arch=arm64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", line_number: 401, key: "\"LIBRARY_SEARCH_PATHS[arch=x86_64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", @@ -820,13 +820,13 @@ Pbxproj { identation: "\t\t\t\t", line_number: 432, key: "\"LIBRARY_SEARCH_PATHS[arch=arm64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", line_number: 433, key: "\"LIBRARY_SEARCH_PATHS[arch=x86_64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", diff --git a/crates/tauri-codegen/CHANGELOG.md b/crates/tauri-codegen/CHANGELOG.md index d874ccd7631f..fa8c1c97352d 100644 --- a/crates/tauri-codegen/CHANGELOG.md +++ b/crates/tauri-codegen/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.6.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` +- [`df05c0056`](https://www.github.com/tauri-apps/tauri/commit/df05c00563a91fc936bd15c6b10dd2825472f96b) Upgraded to `tauri-utils@2.9.0` + ## \[2.5.5] ### Dependencies diff --git a/crates/tauri-codegen/Cargo.toml b/crates/tauri-codegen/Cargo.toml index e1154e67687c..5f49642cbb78 100644 --- a/crates/tauri-codegen/Cargo.toml +++ b/crates/tauri-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.1" description = "code generation meant to be consumed inside of `tauri` through `tauri-build` or `tauri-macros`" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -20,8 +20,8 @@ quote = "1" syn = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ - "build", +tauri-utils = { version = "2.9.1", path = "../tauri-utils", features = [ + "build-2", ] } thiserror = "2" walkdir = "2" diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index d3996f586135..6b907b9c7691 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -25,7 +25,7 @@ use tauri_utils::{ }, assets::AssetKey, config::{Config, FrontendDist, PatternKind}, - html::{NodeRef, inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node}, + html2::{Document, inject_nonce_token, parse_doc, serialize_doc}, platform::Target, tokens::{map_lit, str_lit}, }; @@ -44,27 +44,25 @@ pub struct ContextData { pub test: bool, } -fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) { - if let Ok(inline_script_elements) = document.select("script:not(:empty)") { - let mut scripts = Vec::new(); - for inline_script_el in inline_script_elements { - let script = inline_script_el.as_node().text_contents(); - let mut hasher = Sha256::new(); - hasher.update(tauri_utils::html::normalize_script_for_csp( - script.as_bytes(), - )); - let hash = hasher.finalize(); - scripts.push(format!( - "'sha256-{}'", - base64::engine::general_purpose::STANDARD.encode(hash) - )); - } - csp_hashes - .inline_scripts - .entry(key.clone().into()) - .or_default() - .append(&mut scripts); - } +fn inject_script_hashes(document: &Document, key: &AssetKey, csp_hashes: &mut CspHashes) { + let script_elements = document.select("script:not(:empty)"); + + let scripts = script_elements + .iter() + .map(|element| { + let script = tauri_utils::html2::normalize_script_for_csp(element.text().as_bytes()); + let script_hash = Sha256::digest(script); + let hash_base64 = base64::engine::general_purpose::STANDARD.encode(script_hash); + + format!("'sha256-{hash_base64}'") + }) + .collect::>(); + + csp_hashes + .inline_scripts + .entry(key.clone().into()) + .or_default() + .extend(scripts); } fn map_core_assets( @@ -77,7 +75,7 @@ fn map_core_assets( if path.extension() == Some(OsStr::new("html")) { #[allow(clippy::collapsible_if)] if csp { - let document = parse_html(String::from_utf8_lossy(input).into_owned()); + let document = parse_doc(String::from_utf8_lossy(input).into_owned()); inject_nonce_token(&document, &dangerous_disable_asset_csp_modification); @@ -85,7 +83,7 @@ fn map_core_assets( inject_script_hashes(&document, key, csp_hashes); } - *input = serialize_html_node(&document); + *input = serialize_doc(&document); } } Ok(()) @@ -108,13 +106,13 @@ fn map_isolation( move |key, path, input, csp_hashes| { if path.extension() == Some(OsStr::new("html")) { - let isolation_html = parse_html(String::from_utf8_lossy(input).into_owned()); + let isolation_html = parse_doc(String::from_utf8_lossy(input).into_owned()); // this is appended, so no need to reverse order it - tauri_utils::html::inject_codegen_isolation_script(&isolation_html); + tauri_utils::html2::inject_codegen_isolation_script(&isolation_html); // temporary workaround for windows not loading assets - tauri_utils::html::inline_isolation(&isolation_html, &dir); + tauri_utils::html2::inline_isolation(&isolation_html, &dir); inject_nonce_token( &isolation_html, @@ -125,7 +123,7 @@ fn map_isolation( csp_hashes.styles.push(iframe_style_csp_hash.clone()); - *input = isolation_html.to_string().as_bytes().to_vec() + *input = serialize_doc(&isolation_html) } Ok(()) diff --git a/crates/tauri-codegen/src/embedded_assets.rs b/crates/tauri-codegen/src/embedded_assets.rs index 33a75ebe953a..a2bc7e876259 100644 --- a/crates/tauri-codegen/src/embedded_assets.rs +++ b/crates/tauri-codegen/src/embedded_assets.rs @@ -182,7 +182,7 @@ impl CspHashes { let mut hasher = Sha256::new(); hasher.update( &std::fs::read(path) - .map(|b| tauri_utils::html::normalize_script_for_csp(&b)) + .map(|b| tauri_utils::html2::normalize_script_for_csp(&b)) .map_err(|error| EmbeddedAssetsError::AssetRead { path: path.to_path_buf(), error, diff --git a/crates/tauri-driver/CHANGELOG.md b/crates/tauri-driver/CHANGELOG.md index 7dc771920114..fd67ede86919 100644 --- a/crates/tauri-driver/CHANGELOG.md +++ b/crates/tauri-driver/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## \[2.0.6] + +### What's Changed + +- [`3057eda06`](https://www.github.com/tauri-apps/tauri/commit/3057eda067b87761644209adeec077f232585c5d) ([#15324](https://www.github.com/tauri-apps/tauri/pull/15324)) Support `eq-separator` for `tauri-driver`. + ## \[2.0.5] ### Bug Fixes diff --git a/crates/tauri-driver/Cargo.toml b/crates/tauri-driver/Cargo.toml index 51613e5d9367..e0a3cb027f75 100644 --- a/crates/tauri-driver/Cargo.toml +++ b/crates/tauri-driver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-driver" -version = "2.0.5" +version = "2.0.6" authors = ["Tauri Programme within The Commons Conservancy"] categories = ["gui", "web-programming"] license = "Apache-2.0 OR MIT" @@ -24,7 +24,7 @@ hyper-util = { version = "0.1", features = [ "server", "tokio", ] } -pico-args = "0.5" +pico-args = { version = "0.5", features = ["eq-separator"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["macros"] } diff --git a/crates/tauri-macos-sign/CHANGELOG.md b/crates/tauri-macos-sign/CHANGELOG.md index 577e23e336b4..9502ef75c3bb 100644 --- a/crates/tauri-macos-sign/CHANGELOG.md +++ b/crates/tauri-macos-sign/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## \[2.3.4] + +### Enhancements + +- [`eacd36a4e`](https://www.github.com/tauri-apps/tauri/commit/eacd36a4ea4d6a14a73f414981fb5a8af7dfdafe) ([#15038](https://www.github.com/tauri-apps/tauri/pull/15038)) Do not rely on system base64 CLI to decode certificates. + ## \[2.3.3] ### Dependencies diff --git a/crates/tauri-macos-sign/Cargo.toml b/crates/tauri-macos-sign/Cargo.toml index 0f26ea197e6b..e823400ee65b 100644 --- a/crates/tauri-macos-sign/Cargo.toml +++ b/crates/tauri-macos-sign/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-macos-sign" -version = "2.3.3" +version = "2.3.4" authors = ["Tauri Programme within The Commons Conservancy"] license = "Apache-2.0 OR MIT" keywords = ["codesign", "signing", "macos", "ios", "tauri"] diff --git a/crates/tauri-macros/CHANGELOG.md b/crates/tauri-macros/CHANGELOG.md index 2406a7af5f42..2e3ec6a59577 100644 --- a/crates/tauri-macros/CHANGELOG.md +++ b/crates/tauri-macros/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-codegen@2.6.1` + +## \[2.6.0] + +### New Features + +- [`c00a3dbff`](https://www.github.com/tauri-apps/tauri/commit/c00a3dbffccd6e051d3b7332f706b6c63759865d) ([#14473](https://www.github.com/tauri-apps/tauri/pull/14473)) Add support for the `rename` attribute in the `tauri::command` macro to allow renaming the command to something other than the function name. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` +- Upgraded to `tauri-codegen@2.6.0` + ## \[2.5.5] ### Dependencies diff --git a/crates/tauri-macros/Cargo.toml b/crates/tauri-macros/Cargo.toml index 29ba5044f629..69e7f3891684 100644 --- a/crates/tauri-macros/Cargo.toml +++ b/crates/tauri-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-macros" -version = "2.5.5" +version = "2.6.1" description = "Macros for the tauri crate." exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -20,8 +20,8 @@ proc-macro2 = { version = "1", features = ["span-locations"] } quote = "1" syn = { version = "2", features = ["full"] } heck = "0.5" -tauri-codegen = { version = "2.5.5", default-features = false, path = "../tauri-codegen" } -tauri-utils = { version = "2.8.3", path = "../tauri-utils" } +tauri-codegen = { version = "2.6.1", default-features = false, path = "../tauri-codegen" } +tauri-utils = { version = "2.9.1", path = "../tauri-utils" } [features] custom-protocol = [] diff --git a/crates/tauri-macros/src/command/handler.rs b/crates/tauri-macros/src/command/handler.rs index ce745839fa1b..9ce4e2ddaa49 100644 --- a/crates/tauri-macros/src/command/handler.rs +++ b/crates/tauri-macros/src/command/handler.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use quote::format_ident; +use quote::{format_ident, quote}; use syn::{ Attribute, Ident, Path, Token, parse::{Parse, ParseBuffer, ParseStream}, @@ -151,14 +151,30 @@ impl From for proc_macro::TokenStream { ) -> Self { let cmd = format_ident!("__tauri_cmd__"); let invoke = format_ident!("__tauri_invoke__"); - let (paths, attrs): (Vec, Vec>) = command_defs - .into_iter() - .map(|def| (def.path, def.attrs)) - .unzip(); + let mut paths: Vec = Vec::new(); + let mut attrs: Vec> = Vec::new(); + let mut command_name_macros: Vec = Vec::new(); + for (def, command) in command_defs.into_iter().zip(commands) { + let path = def.path; + let attrs_vec = def.attrs; + + let mut command_name_macro_path = path.clone(); + let last = command_name_macro_path + .segments + .last_mut() + .expect("path has at least one segment"); + last.ident = format_ident!("__tauri_command_name_{command}"); + + paths.push(path); + attrs.push(attrs_vec); + // Call the macro to get the command name string literal + command_name_macros.push(quote!(#command_name_macro_path!())); + } + quote::quote!(move |#invoke| { let #cmd = #invoke.message.command(); match #cmd { - #(#(#attrs)* stringify!(#commands) => #wrappers!(#paths, #invoke),)* + #(#(#attrs)* #command_name_macros => #wrappers!(#paths, #invoke),)* _ => { return false; }, diff --git a/crates/tauri-macros/src/command/wrapper.rs b/crates/tauri-macros/src/command/wrapper.rs index f432ce3ed5e5..d8d213106a63 100644 --- a/crates/tauri-macros/src/command/wrapper.rs +++ b/crates/tauri-macros/src/command/wrapper.rs @@ -40,6 +40,7 @@ struct WrapperAttributes { root: TokenStream2, execution_context: ExecutionContext, argument_case: ArgumentCase, + rename: RenamePolicy, } impl Parse for WrapperAttributes { @@ -48,6 +49,7 @@ impl Parse for WrapperAttributes { root: quote!(::tauri), execution_context: ExecutionContext::Blocking, argument_case: ArgumentCase::Camel, + rename: RenamePolicy::Keep, }; let attrs = Punctuated::::parse_terminated(input)?; @@ -57,23 +59,29 @@ impl Parse for WrapperAttributes { return Err(syn::Error::new(input.span(), "unexpected list input")); } WrapperAttributeKind::Meta(Meta::NameValue(v)) => { - if v.path.is_ident("rename_all") { - if let Expr::Lit(ExprLit { + if v.path.is_ident("rename_all") + && let Expr::Lit(ExprLit { lit: Lit::Str(s), attrs: _, }) = v.value - { - wrapper_attributes.argument_case = match s.value().as_str() { - "snake_case" => ArgumentCase::Snake, - "camelCase" => ArgumentCase::Camel, - _ => { - return Err(syn::Error::new( - s.span(), - "expected \"camelCase\" or \"snake_case\"", - )); - } - }; - } + { + wrapper_attributes.argument_case = match s.value().as_str() { + "snake_case" => ArgumentCase::Snake, + "camelCase" => ArgumentCase::Camel, + _ => { + return Err(syn::Error::new( + s.span(), + "expected \"camelCase\" or \"snake_case\"", + )); + } + }; + } else if v.path.is_ident("rename") + && let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = v.value + { + let lit = s.value(); + wrapper_attributes.rename = RenamePolicy::Rename(quote!(#lit)); } else if v.path.is_ident("root") && let Expr::Lit(ExprLit { lit: Lit::Str(s), @@ -93,7 +101,7 @@ impl Parse for WrapperAttributes { WrapperAttributeKind::Meta(Meta::Path(_)) => { return Err(syn::Error::new( input.span(), - "unexpected input, expected one of `rename_all`, `root`, `async`", + "unexpected input, expected one of `rename_all`, `rename`, `root`, `async`", )); } WrapperAttributeKind::Async => { @@ -119,6 +127,12 @@ enum ArgumentCase { Camel, } +/// The rename policy for the command. +enum RenamePolicy { + Keep, + Rename(TokenStream2), +} + /// The bindings we attach to `tauri::Invoke`. struct Invoke { message: Ident, @@ -137,9 +151,11 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { attrs.execution_context = ExecutionContext::Async; } - // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` + // macros used with `pub use my_macro;` need to be exported with `#[macro_export]`. let maybe_macro_export = match &function.vis { - Visibility::Public(_) | Visibility::Restricted(_) => quote!(#[macro_export]), + Visibility::Public(_) | Visibility::Restricted(_) => { + quote!(#[macro_export]) + } _ => TokenStream2::default(), }; @@ -268,6 +284,17 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { TokenStream2::default() }; + // Always define a hidden macro that returns the externally invoked command name. + // This lets the handler match on the renamed string while the original function + // identifier remains usable in `generate_handler![original_fn_name]`. + let command_name_macro_ident = format_ident!("__tauri_command_name_{}", function.sig.ident); + let command_name_value = if let RenamePolicy::Rename(ref rename) = attrs.rename { + quote!(#rename) + } else { + let ident = &function.sig.ident; + quote!(stringify!(#ident)) + }; + // Rely on rust 2018 edition to allow importing a macro from a path. quote!( #async_command_check @@ -275,6 +302,17 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { #maybe_allow_unused #function + // Command name macro used by the handler for pattern matching. + // This macro returns the command name string literal (renamed or original). + #maybe_allow_unused + #maybe_macro_export + #[doc(hidden)] + macro_rules! #command_name_macro_ident { + () => { + #command_name_value + }; + } + #maybe_allow_unused #maybe_macro_export #[doc(hidden)] @@ -301,7 +339,7 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { // allow the macro to be resolved with the same path as the command function #[allow(unused_imports)] - #visibility use #wrapper; + #visibility use {#wrapper, #command_name_macro_ident}; ) .into() } @@ -465,11 +503,16 @@ fn parse_arg( } let root = &attributes.root; + let command_name = if let RenamePolicy::Rename(r) = &attributes.rename { + quote!(stringify!(#r)) + } else { + quote!(stringify!(#command)) + }; Ok(quote!(#root::ipc::CommandArg::from_command( #root::ipc::CommandItem { plugin: #plugin_name, - name: stringify!(#command), + name: #command_name, key: #key, message: &#message, acl: &#acl, diff --git a/crates/tauri-plugin/CHANGELOG.md b/crates/tauri-plugin/CHANGELOG.md index 93a1115aa31e..8667ba6939ac 100644 --- a/crates/tauri-plugin/CHANGELOG.md +++ b/crates/tauri-plugin/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.6.0] + +### New Features + +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` + ## \[2.5.4] ### Dependencies diff --git a/crates/tauri-plugin/Cargo.toml b/crates/tauri-plugin/Cargo.toml index fe9595762eff..4c946d7d0743 100644 --- a/crates/tauri-plugin/Cargo.toml +++ b/crates/tauri-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.1" description = "Build script and runtime Tauri plugin definitions" authors.workspace = true homepage.workspace = true @@ -19,7 +19,6 @@ build = [ "dep:serde", "dep:serde_json", "dep:glob", - "dep:toml", "dep:plist", "dep:walkdir", ] @@ -28,12 +27,11 @@ runtime = [] [dependencies] anyhow = { version = "1", optional = true } serde = { version = "1", optional = true } -tauri-utils = { version = "2.8.3", default-features = false, features = [ - "build", +tauri-utils = { version = "2.9.1", default-features = false, features = [ + "build-2", ], path = "../tauri-utils" } serde_json = { version = "1", optional = true } glob = { version = "0.3", optional = true } -toml = { version = "0.9", optional = true } schemars = { version = "1", features = ["preserve_order"] } walkdir = { version = "2", optional = true } diff --git a/crates/tauri-plugin/src/build/mobile.rs b/crates/tauri-plugin/src/build/mobile.rs index 2ff795aca796..edccc50cc7de 100644 --- a/crates/tauri-plugin/src/build/mobile.rs +++ b/crates/tauri-plugin/src/build/mobile.rs @@ -5,8 +5,7 @@ //! Mobile-specific build utilities. use std::{ - env::var_os, - fs::{copy, create_dir, create_dir_all, read_to_string, remove_dir_all, write}, + fs::{copy, create_dir, create_dir_all, remove_dir_all}, path::{Path, PathBuf}, }; @@ -17,7 +16,7 @@ use super::{build_var, cfg_alias}; #[cfg(target_os = "macos")] pub fn update_entitlements(f: F) -> Result<()> { if let (Some(project_path), Ok(app_name)) = ( - var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), std::env::var("TAURI_IOS_APP_NAME"), ) { update_plist_file( @@ -34,7 +33,7 @@ pub fn update_entitlements(f: F) -> Result<() #[cfg(target_os = "macos")] pub fn update_info_plist(f: F) -> Result<()> { if let (Some(project_path), Ok(app_name)) = ( - var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), std::env::var("TAURI_IOS_APP_NAME"), ) { update_plist_file( @@ -48,16 +47,9 @@ pub fn update_info_plist(f: F) -> Result<()> Ok(()) } +/// Updates the Android manifest by inserting XML content into a specified parent tag. pub fn update_android_manifest(block_identifier: &str, parent: &str, insert: String) -> Result<()> { - if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { - let manifest_path = project_path.join("app/src/main/AndroidManifest.xml"); - let manifest = read_to_string(&manifest_path)?; - let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert); - if rewritten != manifest { - write(manifest_path, rewritten)?; - } - } - Ok(()) + tauri_utils::build::update_android_manifest(block_identifier, parent, insert) } pub(crate) fn setup( @@ -161,7 +153,7 @@ fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( let path = path.as_ref(); if path.exists() { - let plist_str = read_to_string(path)?; + let plist_str = std::fs::read_to_string(path)?; let mut plist = plist::Value::from_reader(Cursor::new(&plist_str))?; if let Some(dict) = plist.as_dictionary_mut() { f(dict); @@ -170,7 +162,7 @@ fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( plist::to_writer_xml(writer, &plist)?; let new_plist_str = String::from_utf8(plist_buf)?; if new_plist_str != plist_str { - write(path, new_plist_str)?; + std::fs::write(path, new_plist_str)?; } } } @@ -178,72 +170,14 @@ fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( Ok(()) } -fn xml_block_comment(id: &str) -> String { - format!("") -} - -fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String { - let block_comment = xml_block_comment(block_identifier); - - let mut rewritten = Vec::new(); - let mut found_block = false; - let parent_closing_tag = format!(""); - for line in xml.split('\n') { - if line.contains(&block_comment) { - found_block = !found_block; - continue; - } - - // found previous block which should be removed - if found_block { - continue; - } - - if let Some(index) = line.find(&parent_closing_tag) { - let indentation = " ".repeat(index + 4); - rewritten.push(format!("{indentation}{block_comment}")); - for l in contents.split('\n') { - rewritten.push(format!("{indentation}{l}")); - } - rewritten.push(format!("{indentation}{block_comment}")); - } - - rewritten.push(line.to_string()); - } - - rewritten.join("\n") -} - #[cfg(test)] mod tests { #[test] - fn insert_into_xml() { - let manifest = r#" - - - - -"#; - let id = "tauritest"; - let new = super::insert_into_xml(manifest, id, "application", ""); - - let block_id_comment = super::xml_block_comment(id); - let expected = format!( - r#" - - - - {block_id_comment} - - {block_id_comment} - -"# - ); - - assert_eq!(new, expected); - - // assert it's still the same after an empty update - let new = super::insert_into_xml(&expected, id, "application", ""); - assert_eq!(new, expected); + fn update_android_manifest() { + use tauri_utils::build::update_android_manifest; + + // This test would require setting up the environment, so we just verify it compiles + // The actual implementation is tested in tauri-utils + let _result = update_android_manifest("test", "activity", "".to_string()); } } diff --git a/crates/tauri-plugin/src/lib.rs b/crates/tauri-plugin/src/lib.rs index 74c1fb941ed6..4c21a6af46ba 100644 --- a/crates/tauri-plugin/src/lib.rs +++ b/crates/tauri-plugin/src/lib.rs @@ -16,9 +16,9 @@ mod build; mod runtime; #[cfg(feature = "build")] -#[cfg_attr(docsrs, doc(feature = "build"))] +#[cfg_attr(docsrs, doc(cfg(feature = "build")))] pub use build::*; #[cfg(feature = "runtime")] -#[cfg_attr(docsrs, doc(feature = "runtime"))] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] #[allow(unused)] pub use runtime::*; diff --git a/crates/tauri-runtime-cef/Cargo.toml b/crates/tauri-runtime-cef/Cargo.toml index 97d2e102095a..d38028784567 100644 --- a/crates/tauri-runtime-cef/Cargo.toml +++ b/crates/tauri-runtime-cef/Cargo.toml @@ -19,15 +19,16 @@ html5ever = "0.29" raw-window-handle = "0.6" url = "2" http = "1" -cef = { version = "=146.4.1", default-features = false } +cef = { version = "=148.0.0", default-features = false } # Not actually used directly, just locking it. -cef-dll-sys = "=146.4.1" +cef-dll-sys = "=148.0.0" serde = { version = "1", features = ["derive"] } serde_json = "1" kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader" } sha2 = "0.10" base64 = "0.22" dirs = "6" +log = "0.4.21" [target."cfg(windows)".dependencies] windows = { version = "0.61", features = [ diff --git a/crates/tauri-runtime-cef/src/cef_impl.rs b/crates/tauri-runtime-cef/src/cef_impl.rs index d40da9f3606e..91c2ab2d7f70 100644 --- a/crates/tauri-runtime-cef/src/cef_impl.rs +++ b/crates/tauri-runtime-cef/src/cef_impl.rs @@ -9,9 +9,10 @@ use dioxus_debug_cell::RefCell; use sha2::{Digest, Sha256}; use std::{ collections::HashMap, + path::{Component, Path, PathBuf}, sync::{ Arc, Mutex, - atomic::{AtomicBool, AtomicU32, Ordering}, + atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}, mpsc::channel, }, }; @@ -22,18 +23,47 @@ use tauri_runtime::{ Size, }, webview::{InitializationScript, PendingWebview, UriSchemeProtocolHandler, WebviewAttributes}, - window::{PendingWindow, WindowEvent, WindowId}, + window::{DragDropEvent, PendingWindow, WebviewEvent, WindowEvent, WindowId}, }; #[cfg(target_os = "macos")] use tauri_utils::TitleBarStyle; use tauri_utils::html::normalize_script_for_csp; use crate::{ - AppWebview, AppWindow, CefRuntime, CefWindowBuilder, DevToolsProtocolHandler, Message, - RuntimeStyle as CefRuntimeStyle, WebviewAtribute, WebviewMessage, WindowMessage, - cef_webview::CefWebview, + AppWebview, AppWindow, CefRuntime, CefWebviewDispatcher, CefWindowBuilder, + DevToolsProtocolHandler, Message, RuntimeContext, RuntimeStyle as CefRuntimeStyle, + WebviewAtribute, WebviewMessage, WindowMessage, cef_webview::CefWebview, }; +use std::cell::Cell; + +// Tracks whether we're inside a user event callback. When set, `post_message` +// defers through the CEF task runner instead of executing synchronously, to +// avoid Win32 message-pump re-entrancy from APIs like ShowWindow/SetFocus +// or locking a mutex while already locked on the same thread. +thread_local! { + static IN_EVENT_CALLBACK: Cell = const { Cell::new(false) }; +} + +/// Returns true if we're currently inside a user event callback. +pub fn is_in_event_callback() -> bool { + IN_EVENT_CALLBACK.get() +} + +/// Run a function within the context of an event callback, ensuring that [`is_in_event_callback`] returns true for the duration of the callback. +fn in_callback(f: impl FnOnce() -> R) -> R { + struct Guard; + impl Drop for Guard { + fn drop(&mut self) { + IN_EVENT_CALLBACK.set(false); + } + } + + IN_EVENT_CALLBACK.set(true); + let _guard = Guard; + f() +} + mod cookie; mod drag_window; pub mod request_handler; @@ -47,8 +77,119 @@ type CefOsEvent<'a> = *mut u8; #[cfg(windows)] type CefOsEvent<'a> = Option<&'a mut sys::MSG>; type AddressChangedHandler = dyn Fn(&url::Url) + Send + Sync; +type IpcHandler = + dyn Fn(tauri_runtime::webview::DetachedWebview>, http::Request) + Send; +type PendingInitialLoad = (Browser, String); +type PendingInitialLoads = Arc>>; + +const DRAG_DROP_BRIDGE_PATH: &str = "/__tauri_cef_drag_drop__"; +const IPC_MESSAGE_NAME: &str = "tauri:ipc"; +const IPC_POST_MESSAGE_FUNCTION: &str = "postMessage"; +const INITIAL_LOAD_URL: &str = concat!( + "data:text/html;charset=utf-8,", + "%3C!doctype%20html%3E", + "%3Chtml%20data-tauri-cef-internal%3D%22initial-load%22%3E", + "%3Chead%3E", + "%3Cmeta%20charset%3D%22utf-8%22%3E", + "%3Ctitle%3ETauri%20CEF%20Initial%20Load%3C%2Ftitle%3E", + "%3C%2Fhead%3E", + "%3Cbody%20data-tauri-cef-internal%3D%22initial-load%22%3E", + "%3C!--%20Tauri%20CEF%20internal%20initial%20load%20placeholder%20--%3E", + "%3C%2Fbody%3E", + "%3C%2Fhtml%3E", +); +static NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID: AtomicI32 = AtomicI32::new(1_000_000); +const DRAG_DROP_INIT_SCRIPT: &str = r#" +(() => { + if (window.__TAURI_CEF_DRAG_DROP__) { + return; + } + + Object.defineProperty(window, "__TAURI_CEF_DRAG_DROP__", { + value: true, + configurable: false, + }); + + const PATH = "/__tauri_cef_drag_drop__"; + let entered = false; + + const position = (event) => ({ + x: event.clientX * window.devicePixelRatio, + y: event.clientY * window.devicePixelRatio, + }); + + const send = (type, event) => { + const pos = position(event); + const url = new URL(PATH, window.location.href); + url.searchParams.set("payload", JSON.stringify({ type, x: pos.x, y: pos.y })); + fetch(url.href, { + method: "GET", + cache: "no-store", + credentials: "omit", + }).catch(() => {}); + }; + + const listen = (eventName, handler) => { + window.addEventListener(eventName, handler, { capture: true }); + }; + + listen("dragenter", (event) => { + if (!entered) { + entered = true; + send("enter", event); + } + }); + + listen("dragover", (event) => { + if (!entered) { + entered = true; + send("enter", event); + } + send("over", event); + }); + + listen("drop", (event) => { + if (!entered) { + send("enter", event); + } + entered = false; + send("drop", event); + }); + + listen("dragleave", (event) => { + const x = event.clientX; + const y = event.clientY; + if (entered && (x <= 0 || y <= 0 || x >= window.innerWidth || y >= window.innerHeight)) { + entered = false; + send("leave", event); + } + }); +})(); +"#; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DragDropEventTarget { + Window, + Webview, +} + +#[derive(Default)] +struct DragDropState { + paths: Option>, + native_entered: bool, + entered: bool, +} + +#[derive(Clone, serde::Deserialize)] +struct DragDropScriptEvent { + #[serde(rename = "type")] + kind: String, + x: f64, + y: f64, +} /// CEF transparent color value (ARGB) +#[allow(dead_code)] const TRANSPARENT: u32 = 0x00000000; #[inline] @@ -96,6 +237,14 @@ fn rect_to_cef(rect: Rect, scale_factor: f64) -> cef::Rect { } } +#[inline] +fn window_scale_factor(window: &Window) -> f64 { + window + .display() + .map(|d| d.device_scale_factor() as f64) + .unwrap_or(1.0) +} + #[inline] fn theme_to_color_variant(theme: Option) -> ColorVariant { match theme { @@ -353,6 +502,49 @@ fn hash_script(script: &str) -> String { ) } +fn initialization_scripts_from_webview_attributes( + webview_attributes: &mut WebviewAttributes, +) -> Arc> { + let mut initialization_scripts = Vec::new(); + + if webview_attributes.drag_drop_handler_enabled { + initialization_scripts.push(CefInitScript::new(InitializationScript { + script: DRAG_DROP_INIT_SCRIPT.to_string(), + for_main_frame_only: false, + })); + } + + initialization_scripts.extend( + std::mem::take(&mut webview_attributes.initialization_scripts) + .into_iter() + .map(CefInitScript::new), + ); + + Arc::new(initialization_scripts) +} + +fn collect_drag_data_paths(drag_data: &mut DragData) -> Vec { + let mut paths = CefStringList::new(); + if drag_data.file_paths(Some(&mut paths)) != 0 { + let paths = paths + .into_iter() + .filter(|path| !path.is_empty()) + .map(PathBuf::from) + .collect::>(); + + if !paths.is_empty() { + return paths; + } + } + + let file_name = CefStringUtf16::from(&drag_data.file_name()).to_string(); + if file_name.is_empty() { + Vec::new() + } else { + vec![PathBuf::from(file_name)] + } +} + pub type SchemeHandlerRegistry = Arc< Mutex< HashMap< @@ -377,6 +569,16 @@ pub struct Context { pub next_window_event_id: Arc, pub next_webview_event_id: Arc, pub scheme_handler_registry: SchemeHandlerRegistry, + /// Root cache path passed to [`cef::Settings::cache_path`] during + /// [`cef::initialize`]. Per-webview request context cache paths must be + /// equal to or a child of this directory. + pub cache_path: Arc, + /// Set once an `ExitRequested` has been approved and the runtime is in the + /// asynchronous tear-down phase. While set, per-window close events + /// (`CloseRequested`, `Destroyed`) and any further `ExitRequested`/`Exit` + /// emissions are suppressed so the public event sequence stays at + /// `ExitRequested -> Exit` for direct exits. + pub is_shutting_down: Arc, } impl Context { @@ -397,6 +599,14 @@ impl Context { } } +fn runtime_context(context: &Context) -> RuntimeContext { + RuntimeContext { + main_thread_task_runner: cef::task_runner_get_for_current_thread().expect("null task runner"), + main_thread_id: std::thread::current().id(), + cef_context: context.clone(), + } +} + wrap_app! { pub struct TauriApp { context: Context, @@ -413,6 +623,10 @@ wrap_app! { )) } + fn render_process_handler(&self) -> Option { + Some(TauriRenderProcessHandler::new()) + } + fn on_before_command_line_processing( &self, _process_type: Option<&CefString>, @@ -458,28 +672,138 @@ wrap_browser_process_handler! { let mut list = CefStringList::new(); command_line.arguments(Some(&mut list)); let args: Vec = list.into_iter().collect(); - if args.len() == 1 - && let Ok(url) = url::Url::parse(&args[0]) { - let scheme = url.scheme().to_string(); - if self.deep_link_schemes.iter().any(|s| s == &scheme) { - (self.context.callback.borrow())(RunEvent::Opened { - urls: vec![url], - }); - return 1; - } + if let Ok(url) = url::Url::parse(&args[0]) { + let scheme = url.scheme().to_string(); + if self.deep_link_schemes.iter().any(|s| s == &scheme) { + (self.context.callback.borrow())(RunEvent::Opened { + urls: vec![url], + }); + return 1; } + } // TODO: add event 1 } } } +wrap_v8_handler! { + struct IpcPostMessageV8Handler; + + impl V8Handler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + retval: Option<&mut Option>, + exception: Option<&mut CefString>, + ) -> std::os::raw::c_int { + let Some(name) = name else { + return 0; + }; + if name.to_string() != IPC_POST_MESSAGE_FUNCTION { + return 0; + } + + let Some(message) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|argument| argument.is_string() != 0) + else { + if let Some(exception) = exception { + *exception = CefString::from("window.ipc.postMessage expects a string argument"); + } + return 1; + }; + + let Some(context) = v8_context_get_current_context() else { + return 1; + }; + let Some(frame) = context.frame() else { + return 1; + }; + + let body = CefString::from(&message.string_value()).to_string(); + let url = CefString::from(&frame.url()).to_string(); + let mut process_message = process_message_create(Some(&CefString::from(IPC_MESSAGE_NAME))); + if let Some(args) = process_message.as_ref().and_then(ProcessMessage::argument_list) { + args.set_string(0, Some(&CefString::from(url.as_str()))); + args.set_string(1, Some(&CefString::from(body.as_str()))); + frame.send_process_message(ProcessId::BROWSER, process_message.as_mut()); + } + + if let Some(retval) = retval { + *retval = v8_value_create_undefined(); + } + 1 + } + } +} + +fn install_ipc_post_message(context: Option<&mut V8Context>) { + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + + let Some(mut ipc) = v8_value_create_object(None, None) else { + return; + }; + let mut handler = IpcPostMessageV8Handler::new(); + let post_message_name = CefString::from(IPC_POST_MESSAGE_FUNCTION); + let Some(mut post_message) = + v8_value_create_function(Some(&post_message_name), Some(&mut handler)) + else { + return; + }; + + ipc.set_value_bykey( + Some(&post_message_name), + Some(&mut post_message), + attributes, + ); + window.set_value_bykey(Some(&CefString::from("ipc")), Some(&mut ipc), attributes); +} + +wrap_render_process_handler! { + struct TauriRenderProcessHandler; + + impl RenderProcessHandler { + fn on_context_created( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + install_ipc_post_message(context); + } + } +} + +wrap_app! { + pub struct TauriRenderApp; + + impl App { + fn render_process_handler(&self) -> Option { + Some(TauriRenderProcessHandler::new()) + } + } +} + wrap_load_handler! { struct BrowserLoadHandler { - initialization_scripts: Arc>, on_page_load_handler: Option>, - custom_scheme_domain_names: Vec, - custom_protocol_scheme: String, } impl LoadHandler { @@ -508,7 +832,7 @@ wrap_load_handler! { &self, _browser: Option<&mut Browser>, frame: Option<&mut Frame>, - http_status_code: ::std::os::raw::c_int, + _http_status_code: ::std::os::raw::c_int, ) { let Some(frame) = frame else { return }; @@ -520,53 +844,32 @@ wrap_load_handler! { handler(url, tauri_runtime::webview::PageLoadEvent::Finished); } } + } + } +} - // run init scripts for http/https pages that are not custom schemes - // custom schemes are handled by the request handler - // where we inject scripts directly in the html - - if !(200..300).contains(&http_status_code) { - return; - } - - let url = frame.url(); - let url_str = cef::CefString::from(&url).to_string(); - let url_obj = url::Url::parse(&url_str).ok(); - - let is_custom_scheme_url = url_obj - .as_ref() - .map(|u| { - let scheme = u.scheme(); - if scheme == self.custom_protocol_scheme { - let host_str = u.host_str().unwrap_or("").to_string(); - scheme == self.custom_protocol_scheme && self.custom_scheme_domain_names.contains(&host_str) - } else { - false - } - }); - // if we can't parse the URL, also return - if is_custom_scheme_url.unwrap_or(true) { return; } - - let is_main_frame = frame.is_main() == 1; - - let scripts_to_execute = if is_main_frame { - Box::new(self.initialization_scripts.iter().map(|s| &s.script.script)) as Box> - } else { - Box::new(self.initialization_scripts - .iter() - .filter(|s| !s.script.for_main_frame_only) - .map(|s| &s.script.script)) as Box> - }; - - for script in scripts_to_execute { - let script_url = format!("{}://__tauri_init_script__", url_obj.as_ref().map(|u| u.scheme()).unwrap_or("http")); +wrap_drag_handler! { + struct BrowserDragHandler { + drag_drop_state: Arc>, + } - frame.execute_java_script( - Some(&cef::CefString::from(script.as_str())), - Some(&cef::CefString::from(script_url.as_str())), - 0, - ); - } + impl DragHandler { + fn on_drag_enter( + &self, + _browser: Option<&mut Browser>, + drag_data: Option<&mut DragData>, + _mask: DragOperationsMask, + ) -> ::std::os::raw::c_int { + let mut state = self.drag_drop_state.lock().unwrap(); + state.entered = false; + state.paths = drag_data + .map(collect_drag_data_paths) + .filter(|paths| !paths.is_empty()); + state.native_entered = state.paths.is_some(); + + // Let Chromium continue with the drag operation so the injected script can + // report over/drop/leave with accurate viewport positions. + 0 } } } @@ -603,6 +906,11 @@ wrap_display_handler! { let Some(handler) = &self.address_changed_handler else { return }; let Some(url) = url else { return }; let url_str = url.to_string(); + + if url_str == INITIAL_LOAD_URL { + return; + } + let Ok(parsed) = url::Url::parse(&url_str) else { return }; handler(&parsed); } @@ -633,6 +941,7 @@ wrap_context_menu_handler! { cef::wrap_dev_tools_message_observer! { struct TauriDevToolsProtocolObserver { handlers: Arc>>>, + pending_initial_loads: PendingInitialLoads, } impl DevToolsMessageObserver { @@ -659,6 +968,12 @@ cef::wrap_dev_tools_message_observer! { success: std::os::raw::c_int, result: Option<&[u8]>, ) { + if let Some((browser, initial_url)) = + self.pending_initial_loads.lock().unwrap().remove(&message_id) + { + post_load_initial_url(browser, initial_url); + } + let protocol = crate::DevToolsProtocol::MethodResult { message_id, success: success != 0, @@ -692,19 +1007,222 @@ cef::wrap_dev_tools_message_observer! { } } +fn runtime_evaluate_result_to_json(result: Option<&[u8]>) -> String { + let Some(result) = result else { + return String::new(); + }; + let Ok(result) = serde_json::from_slice::(result) else { + return String::new(); + }; + + if result.get("exceptionDetails").is_some() { + return String::new(); + } + + let remote_object = result.get("result").unwrap_or(&result); + remote_object + .get("value") + .and_then(|value| serde_json::to_string(value).ok()) + .unwrap_or_default() +} + +cef::wrap_dev_tools_message_observer! { + struct EvalScriptWithCallbackDevToolsObserver { + message_id: Arc, + callback: Arc>>>, + registration: Arc>>, + } + + impl DevToolsMessageObserver { + fn on_dev_tools_method_result( + &self, + _browser: Option<&mut Browser>, + message_id: std::os::raw::c_int, + success: std::os::raw::c_int, + result: Option<&[u8]>, + ) { + if message_id != self.message_id.load(Ordering::Relaxed) { + return; + } + + let Some(callback) = self.callback.lock().unwrap().take() else { + return; + }; + + let result = if success != 0 { + runtime_evaluate_result_to_json(result) + } else { + String::new() + }; + callback(result); + + let _ = self.registration.lock().unwrap().take(); + } + } +} + /// Registers a DevTools protocol observer. Returns the [`cef::Registration`] which must be /// kept alive for the observer to stay registered. The observer is unregistered when /// the Registration is dropped. fn add_dev_tools_observer( browser: &cef::Browser, handlers: Arc>>>, + pending_initial_loads: PendingInitialLoads, ) -> Option { browser.host().and_then(|host| { - let mut observer = TauriDevToolsProtocolObserver::new(handlers); + let mut observer = TauriDevToolsProtocolObserver::new(handlers, pending_initial_loads); host.add_dev_tools_message_observer(Some(&mut observer)) }) } +fn devtools_initialization_script_source( + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], +) -> Option { + if initialization_scripts.is_empty() { + return None; + } + + let custom_protocol = serde_json::to_string(&format!("{custom_protocol_scheme}:")).ok()?; + let custom_domains = serde_json::to_string(custom_scheme_domain_names).ok()?; + let mut source = format!( + r#"{{ + const __TAURI_CEF_INIT_CUSTOM_PROTOCOL__ = {custom_protocol}; + const __TAURI_CEF_INIT_CUSTOM_DOMAINS__ = new Set({custom_domains}); + const __TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__ = + location.protocol === __TAURI_CEF_INIT_CUSTOM_PROTOCOL__ + && __TAURI_CEF_INIT_CUSTOM_DOMAINS__.has(location.hostname); + const __TAURI_CEF_INIT_IS_MAIN_FRAME__ = (() => {{ + try {{ + return window.top === window; + }} catch (_) {{ + return false; + }} + }})(); +"# + ); + + for init_script in initialization_scripts { + source.push_str(" if (!__TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__"); + if init_script.script.for_main_frame_only { + source.push_str(" && __TAURI_CEF_INIT_IS_MAIN_FRAME__"); + } + source.push_str(") {\n"); + source.push_str(init_script.script.script.as_str()); + source.push_str("\n }\n"); + } + + source.push_str("}\n"); + Some(source) +} + +fn register_initialization_scripts( + browser: &Browser, + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], + initial_url: String, + pending_initial_loads: &PendingInitialLoads, +) -> bool { + let Some(source) = devtools_initialization_script_source( + initialization_scripts, + custom_protocol_scheme, + custom_scheme_domain_names, + ) else { + return false; + }; + let Some(host) = browser.host() else { + return false; + }; + + let page_enable_message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); + let page_enable_message = serde_json::json!({ + "id": page_enable_message_id, + "method": "Page.enable", + "params": {} + }) + .to_string(); + let _ = host.send_dev_tools_message(Some(page_enable_message.as_bytes())); + + let message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); + let message = serde_json::json!({ + "id": message_id, + "method": "Page.addScriptToEvaluateOnNewDocument", + "params": { + "source": source, + } + }) + .to_string(); + + pending_initial_loads + .lock() + .unwrap() + .insert(message_id, (browser.clone(), initial_url)); + if host.send_dev_tools_message(Some(message.as_bytes())) == 1 { + true + } else { + pending_initial_loads.lock().unwrap().remove(&message_id); + false + } +} + +wrap_task! { + struct LoadInitialUrlTask { + browser: Browser, + initial_url: String, + } + + impl Task { + fn execute(&self) { + load_initial_url(&self.browser, &self.initial_url); + } + } +} + +fn post_load_initial_url(browser: Browser, initial_url: String) { + let mut task = LoadInitialUrlTask::new(browser, initial_url); + cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); +} + +// Browsers are created with an inert internal document so the BrowserHost exists +// before the app's real first navigation starts. That gives us a chance to +// register the CDP document-start script for remote/cross-site navigations; the +// custom-protocol path still injects into HTML because CEF does not apply this +// CDP hook to those documents reliably. +// +// The real load is posted as a CEF UI task instead of performed inline. This +// keeps the browser creation/CDP setup stack from re-entering navigation. +fn load_initial_url_after_registering_initialization_scripts( + browser: &Browser, + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], + initial_url: &str, + pending_initial_loads: &PendingInitialLoads, +) { + let browser_for_callback = browser.clone(); + let initial_url = initial_url.to_string(); + let is_waiting_for_initialization_scripts = register_initialization_scripts( + browser, + initialization_scripts, + custom_protocol_scheme, + custom_scheme_domain_names, + initial_url.clone(), + pending_initial_loads, + ); + + if !is_waiting_for_initialization_scripts { + post_load_initial_url(browser_for_callback, initial_url); + } +} + +fn load_initial_url(browser: &Browser, initial_url: &str) { + if let Some(frame) = browser.main_frame() { + frame.load_url(Some(&CefString::from(initial_url))); + } +} + wrap_keyboard_handler! { struct BrowserKeyboardHandler { devtools_enabled: bool, @@ -1082,7 +1600,12 @@ wrap_client! { struct BrowserClient { window_kind: WindowKind, window_id: WindowId, - initialization_scripts: Arc>, + webview_id: u32, + label: String, + drag_drop_event_target: DragDropEventTarget, + drag_drop_handler_enabled: bool, + drag_drop_state: Arc>, + ipc_handler: Option>>, on_page_load_handler: Option>, document_title_changed_handler: Option>, navigation_handler: Option>, @@ -1090,17 +1613,27 @@ wrap_client! { new_window_handler: Option>>>, download_handler: Option>, devtools_enabled: bool, - custom_scheme_domain_names: Vec, - custom_protocol_scheme: String, context: Context, + runtime_context: RuntimeContext, initial_url: Option, } impl Client { + fn drag_handler(&self) -> Option { + self + .drag_drop_handler_enabled + .then(|| BrowserDragHandler::new(self.drag_drop_state.clone())) + } + fn request_handler(&self) -> Option { Some(request_handler::WebRequestHandler::new( - self.initialization_scripts.clone(), self.navigation_handler.clone(), + self.context.clone(), + self.window_id, + self.webview_id, + self.drag_drop_event_target, + self.drag_drop_handler_enabled, + self.drag_drop_state.clone(), )) } @@ -1115,12 +1648,7 @@ wrap_client! { } fn load_handler(&self) -> Option { - Some(BrowserLoadHandler::new( - self.initialization_scripts.clone(), - self.on_page_load_handler.clone(), - self.custom_scheme_domain_names.clone(), - self.custom_protocol_scheme.clone(), - )) + Some(BrowserLoadHandler::new(self.on_page_load_handler.clone())) } fn display_handler(&self) -> Option { @@ -1145,8 +1673,57 @@ wrap_client! { fn permission_handler(&self) -> Option { Some(BrowserPermissionHandler::new()) } - } -} + + fn on_process_message_received( + &self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + source_process: ProcessId, + message: Option<&mut ProcessMessage>, + ) -> std::os::raw::c_int { + if source_process != ProcessId::RENDERER { + return 0; + } + + let Some(message) = message else { + return 0; + }; + if CefString::from(&message.name()).to_string() != IPC_MESSAGE_NAME { + return 0; + } + + let Some(handler) = &self.ipc_handler else { + return 1; + }; + let Some(args) = message.argument_list() else { + return 1; + }; + + let mut url = CefString::from(&args.string(0)).to_string(); + if url.is_empty() + && let Some(frame) = frame { + url = CefString::from(&frame.url()).to_string(); + } + let body = CefString::from(&args.string(1)).to_string(); + + if let Ok(request) = http::Request::builder().uri(url).body(body) { + handler( + tauri_runtime::webview::DetachedWebview { + label: self.label.clone(), + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(self.window_id)), + webview_id: self.webview_id, + context: self.runtime_context.clone(), + }, + }, + request, + ); + } + + 1 + } + } +} wrap_browser_view_delegate! { struct BrowserViewDelegateImpl { @@ -1156,6 +1733,10 @@ wrap_browser_view_delegate! { webview_label: String, uri_scheme_protocols: Arc>>>, initialization_scripts: Arc>, + custom_protocol_scheme: String, + custom_scheme_domain_names: Vec, + initial_url: String, + pending_initial_loads: PendingInitialLoads, devtools_protocol_handlers: Arc>>>, devtools_observer_registration: Arc>>, webview_attributes: Arc>, @@ -1185,12 +1766,6 @@ wrap_browser_view_delegate! { let real_id = browser.identifier(); let _ = std::mem::replace(&mut *self.browser_id.borrow_mut(), real_id); - // Only add the observer when at least one listener is registered - if !self.devtools_protocol_handlers.lock().unwrap().is_empty() - && let Some(registration) = add_dev_tools_observer(browser, self.devtools_protocol_handlers.clone()) { - self.devtools_observer_registration.lock().unwrap().replace(registration); - } - let mut registry = self.scheme_handler_registry.lock().unwrap(); for (scheme, handler) in self.uri_scheme_protocols.iter() { registry.insert( @@ -1202,6 +1777,27 @@ wrap_browser_view_delegate! { ), ); } + drop(registry); + + { + let mut devtools_observer_registration = self.devtools_observer_registration.lock().unwrap(); + if devtools_observer_registration.is_none() + && let Some(registration) = + add_dev_tools_observer(browser, self.devtools_protocol_handlers.clone(), self.pending_initial_loads.clone()) + { + devtools_observer_registration.replace(registration); + } + } + + load_initial_url_after_registering_initialization_scripts( + browser, + &self.initialization_scripts, + &self.custom_protocol_scheme, + &self.custom_scheme_domain_names, + &self.initial_url, + &self.pending_initial_loads, + ); + } } @@ -1225,6 +1821,7 @@ wrap_window_delegate! { attributes: Arc>, last_emitted_position: RefCell>, last_emitted_size: RefCell>, + last_emitted_scale_factor: RefCell, suppress_next_theme_changed: RefCell, context: Context } @@ -1303,16 +1900,17 @@ wrap_window_delegate! { } fn on_theme_changed(&self, view: Option<&mut View>) { - let Some(view) = view else { return; }; - let attrs = self.attributes.borrow(); #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - if attrs.transparent.unwrap_or_default() { - view.set_background_color(TRANSPARENT); - } else if let Some(color) = attrs.background_color { - let color = color_to_cef_argb(color); - view.set_background_color(color); + { + let Some(view) = view else { return; }; + if attrs.transparent.unwrap_or_default() { + view.set_background_color(TRANSPARENT); + } else if let Some(color) = attrs.background_color { + let color = color_to_cef_argb(color); + view.set_background_color(color); + } } // macOS resets traffic light button positions during the layout pass @@ -1378,6 +1976,8 @@ wrap_window_delegate! { impl WindowDelegate { fn on_window_created(&self, window: Option<&mut Window>) { if let Some(window) = window { + *self.last_emitted_scale_factor.borrow_mut() = window_scale_factor(window); + // Setup necessary handling for `start_window_dragging` to work on Windows #[cfg(windows)] drag_window::windows::subclass_window_for_dragging(window); @@ -1546,7 +2146,12 @@ wrap_window_delegate! { } fn can_close(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - if self.force_close.load(Ordering::SeqCst) { + // Direct-exit tear-down: behave like cefclient with `force_close = true` + // — skip the embedder dialog and just drive the cooperative browser + // close so `OnBeforeClose` fires. + if self.context.is_shutting_down.load(Ordering::SeqCst) + || self.force_close.load(Ordering::SeqCst) + { close_window_browsers(self.window_id, &self.windows); return 1; } @@ -1627,10 +2232,33 @@ wrap_window_delegate! { inner.set_bounds(Some(&rect)); } - let scale = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); + let scale = window_scale_factor(window); + + #[cfg(not(windows))] + let physical_size = size.to_physical::(scale); + + #[cfg(windows)] + let physical_size = size; + + let scale_factor_changed = { + let mut emitted_scale_factor = self.last_emitted_scale_factor.borrow_mut(); + let changed = *emitted_scale_factor != scale; + if changed { + *emitted_scale_factor = scale; + } + changed + }; + if scale_factor_changed { + send_window_event( + self.window_id, + &self.windows, + &self.callback, + WindowEvent::ScaleFactorChanged { + scale_factor: scale, + new_inner_size: physical_size, + }, + ); + } let physical_position = LogicalPosition::new(bounds.x, bounds.y) .to_physical::(scale); @@ -1651,10 +2279,6 @@ wrap_window_delegate! { ); } - let physical_size = LogicalSize::new( - bounds.width as u32, - bounds.height as u32, - ).to_physical::(scale); let size_changed = { let mut emitted_size = self.last_emitted_size.borrow_mut(); let changed = *emitted_size != physical_size; @@ -1750,6 +2374,44 @@ fn handle_webview_message( ); } } + WebviewMessage::EvaluateScriptWithCallback(script, callback) => { + if let Some(host) = get_browser(context, window_id, webview_id).and_then(|b| b.host()) { + let message_id = context.next_webview_event_id() as i32 + 1; + let message_id = Arc::new(AtomicI32::new(message_id)); + let callback = Arc::new(Mutex::new(Some(callback))); + let registration = Arc::new(Mutex::new(None)); + let mut observer = EvalScriptWithCallbackDevToolsObserver::new( + message_id.clone(), + callback.clone(), + registration.clone(), + ); + + if let Some(observer_registration) = + host.add_dev_tools_message_observer(Some(&mut observer)) + { + *registration.lock().unwrap() = Some(observer_registration); + + let message = serde_json::json!({ + "id": message_id.load(Ordering::Relaxed), + "method": "Runtime.evaluate", + "params": { + "expression": script, + "returnByValue": true, + } + }) + .to_string(); + + if host.send_dev_tools_message(Some(message.as_bytes())) != 1 { + let _ = registration.lock().unwrap().take(); + if let Some(callback) = callback.lock().unwrap().take() { + callback(String::new()); + } + } + } else if let Some(callback) = callback.lock().unwrap().take() { + callback(String::new()); + } + } + } WebviewMessage::Navigate(url) => { if let Some(frame) = get_main_frame(context, window_id, webview_id) { frame.load_url(Some(&cef::CefString::from(url.as_str()))) @@ -2203,13 +2865,24 @@ fn handle_webview_message( WebviewMessage::OnDevToolsProtocol(handler, tx) => { let result = match get_webview(context, window_id, webview_id) { Some(webview) => { - let mut handlers = webview.devtools_protocol_handlers.lock().unwrap(); - handlers.push(handler); - // Add the observer when the first listener is registered - if handlers.len() == 1 + webview + .devtools_protocol_handlers + .lock() + .unwrap() + .push(handler); + + let needs_devtools_observer = webview + .devtools_observer_registration + .lock() + .unwrap() + .is_none(); + if needs_devtools_observer && let Some(browser) = get_browser(context, window_id, webview_id) - && let Some(registration) = - add_dev_tools_observer(&browser, webview.devtools_protocol_handlers.clone()) + && let Some(registration) = add_dev_tools_observer( + &browser, + webview.devtools_protocol_handlers.clone(), + Arc::new(Mutex::new(HashMap::new())), + ) { *webview.devtools_observer_registration.lock().unwrap() = Some(registration); } @@ -2489,7 +3162,7 @@ fn handle_window_message( ) { match message { WindowMessage::Close => { - on_close_requested(window_id, &context.windows, &context.callback); + on_close_requested(window_id, context); } WindowMessage::Destroy => { on_window_close(window_id, &context.windows); @@ -3155,22 +3828,33 @@ pub fn handle_message(context: &Context, message: Message) { message, } => handle_webview_message(context, window_id, webview_id, message), Message::RequestExit(code) => { + // Direct-exit path (e.g. `request_exit`, macOS `-terminate:`): emit + // only `ExitRequested -> Exit`, matching the cefclient terminate flow + // where `CloseAllWindows` is initiated only after the embedder agrees + // to quit. Skip if we're already shutting down to avoid re-prompting. + if context.is_shutting_down.load(Ordering::SeqCst) { + return; + } + let (tx, rx) = channel(); - (context.callback.borrow())(RunEvent::ExitRequested { - code: Some(code), - tx, + in_callback(|| { + (context.callback.borrow())(RunEvent::ExitRequested { + code: Some(code), + tx, + }); }); let recv = rx.try_recv(); let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); if !should_prevent { - (context.callback.borrow())(RunEvent::Exit); + context.is_shutting_down.store(true, Ordering::SeqCst); + in_callback(|| (context.callback.borrow())(RunEvent::Exit)); } } Message::Task(t) => t(), Message::UserEvent(evt) => { - (context.callback.borrow())(RunEvent::UserEvent(evt)); + in_callback(|| (context.callback.borrow())(RunEvent::UserEvent(evt))); } Message::Noop => {} } @@ -3203,7 +3887,7 @@ fn create_browser_window( mut webview_attributes, platform_specific_attributes: _, uri_scheme_protocols, - ipc_handler: _, + ipc_handler, navigation_handler, new_window_handler, document_title_changed_handler, @@ -3212,21 +3896,23 @@ fn create_browser_window( web_resource_request_handler: _, mut on_page_load_handler, download_handler, + // TODO + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate_handler: _, } = webview; let address_changed_handler = address_changed_handler .map(|h| Arc::new(move |url: &url::Url| h(url)) as Arc); - let initialization_scripts = std::mem::take(&mut webview_attributes.initialization_scripts) - .into_iter() - .map(CefInitScript::new) - .collect::>(); - let initialization_scripts = Arc::new(initialization_scripts); + let drag_drop_handler_enabled = webview_attributes.drag_drop_handler_enabled; + let initialization_scripts = + initialization_scripts_from_webview_attributes(&mut webview_attributes); let on_page_load_handler = on_page_load_handler.take().map(Arc::from); let document_title_changed_handler = document_title_changed_handler.map(Arc::from); let navigation_handler = navigation_handler.map(Arc::from); let new_window_handler = new_window_handler.map(Arc::from); + let ipc_handler: Option>> = ipc_handler.map(Arc::from); let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) && webview_attributes.devtools.unwrap_or(true); @@ -3235,15 +3921,8 @@ fn create_browser_window( "https" } else { "http" - }; - - // Build cached domain names for custom schemes and clone protocols for storage - // before uri_scheme_protocols is moved - let scheme_keys: Vec = uri_scheme_protocols.keys().cloned().collect(); - let custom_scheme_domain_names: Vec = scheme_keys - .iter() - .map(|scheme| format!("{scheme}.localhost")) - .collect(); + } + .to_string(); let uri_scheme_protocols: HashMap>> = uri_scheme_protocols @@ -3252,29 +3931,28 @@ fn create_browser_window( .collect(); let custom_schemes = uri_scheme_protocols.keys().cloned().collect::>(); - - let mut request_context = request_context_from_webview_attributes( - context, - &webview_attributes, - &custom_schemes, - custom_protocol_scheme, - &initialization_scripts, - ); - apply_request_context_theme_scheme(request_context.as_ref(), window_builder.theme); - - let browser_settings = browser_settings_from_webview_attributes(&webview_attributes); + let custom_scheme_domain_names = custom_schemes + .iter() + .map(|scheme| format!("{scheme}.localhost")) + .collect::>(); // Create the AppWindow with BrowserWindow variant before creating the browser let force_close = Arc::new(AtomicBool::new(false)); let attributes = Arc::new(RefCell::new(window_builder)); let initial_url = url.clone(); - let url = CefString::from(url.as_str()); + let url = CefString::from(INITIAL_LOAD_URL); + let drag_drop_state = Arc::new(Mutex::new(DragDropState::default())); - let mut client = BrowserClient::new( + let client = BrowserClient::new( WindowKind::Browser, window_id, - initialization_scripts.clone(), + webview_id, + webview_label.clone(), + DragDropEventTarget::Window, + drag_drop_handler_enabled, + drag_drop_state, + ipc_handler, on_page_load_handler, document_title_changed_handler, navigation_handler, @@ -3282,98 +3960,138 @@ fn create_browser_window( new_window_handler, download_handler, devtools_enabled, - custom_scheme_domain_names.clone(), - custom_protocol_scheme.to_string(), context.clone(), - Some(initial_url), + runtime_context(context), + None, ); - let mut bounds = cef::Rect { - x: 0, - y: 0, - width: 800, - height: 600, - }; - let device_scale_factor = display_get_primary() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.); - if let Some(size) = attributes.borrow().inner_size { - let size = size_to_cef(size, device_scale_factor); - bounds.width = size.width; - bounds.height = size.height; - } - if let Some(position) = attributes.borrow().position { - let position = position_to_cef(position, device_scale_factor); - bounds.x = position.x; - bounds.y = position.y; - } + let webview_attributes = Arc::new(RefCell::new(webview_attributes)); + + // See `create_webview` for why browser creation is deferred to the + // request context's `on_request_context_initialized` callback, and why we + // synchronously pump the message loop afterwards. + let (init_done, on_initialized) = deferred_init_continuation({ + let context = context.clone(); + let webview_attributes = webview_attributes.clone(); + let initialization_scripts = initialization_scripts.clone(); + let custom_protocol_scheme = custom_protocol_scheme.clone(); + let attributes = attributes.clone(); + let force_close = force_close.clone(); + let mut client = client; + move |mut request_context| { + let theme = attributes.borrow().theme; + apply_request_context_theme_scheme(request_context.as_ref(), theme); + + let browser_settings = browser_settings_from_webview_attributes(&webview_attributes.borrow()); + + let mut bounds = cef::Rect { + x: 0, + y: 0, + width: 800, + height: 600, + }; + let device_scale_factor = display_get_primary() + .map(|d| d.device_scale_factor() as f64) + .unwrap_or(1.); + if let Some(size) = attributes.borrow().inner_size { + let size = size_to_cef(size, device_scale_factor); + bounds.width = size.width; + bounds.height = size.height; + } + if let Some(position) = attributes.borrow().position { + let position = position_to_cef(position, device_scale_factor); + bounds.x = position.x; + bounds.y = position.y; + } - let window_info = cef::WindowInfo { - bounds, - ..Default::default() - }; + let window_info = cef::WindowInfo { + bounds, + ..Default::default() + }; - let Some(browser) = browser_host_create_browser_sync( - Some(&window_info), - Some(&mut client), - Some(&url), - Some(&browser_settings), - None, - request_context.as_mut(), - ) else { - eprintln!("Failed to create browser"); - return; - }; + let Some(browser) = browser_host_create_browser_sync( + Some(&window_info), + Some(&mut client), + Some(&url), + Some(&browser_settings), + None, + request_context.as_mut(), + ) else { + eprintln!("Failed to create browser"); + return; + }; + let browser_id_val = browser.identifier(); - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< - Arc, - >::new())); - let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( - &browser, - devtools_protocol_handlers.clone(), - ))); + { + let mut registry = context.scheme_handler_registry.lock().unwrap(); + for (scheme, handler) in &uri_scheme_protocols { + registry.insert( + (browser_id_val, scheme.clone()), + ( + webview_label.clone(), + handler.clone(), + initialization_scripts.clone(), + ), + ); + } + } + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< + Arc, + >::new())); + let pending_initial_loads = Arc::new(Mutex::new(HashMap::new())); + let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser, + devtools_protocol_handlers.clone(), + pending_initial_loads.clone(), + ))); + + load_initial_url_after_registering_initialization_scripts( + &browser, + &initialization_scripts, + &custom_protocol_scheme, + &custom_scheme_domain_names, + &initial_url, + &pending_initial_loads, + ); - let browser = CefWebview::Browser(browser); - let browser_id_val = browser.browser_id(); + let browser = CefWebview::Browser(browser); - { - let mut registry = context.scheme_handler_registry.lock().unwrap(); - for (scheme, handler) in &uri_scheme_protocols { - registry.insert( - (browser_id_val, scheme.clone()), - ( - webview_label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), + context.windows.borrow_mut().insert( + window_id, + AppWindow { + label, + window: crate::AppWindowKind::BrowserWindow, + force_close, + attributes, + webviews: vec![AppWebview { + webview_id, + browser_id: Arc::new(RefCell::new(browser_id_val)), + label: webview_label, + inner: browser, + bounds: Arc::new(Mutex::new(None)), + devtools_enabled, + uri_scheme_protocols: Arc::new(uri_scheme_protocols), + initialization_scripts, + devtools_protocol_handlers, + devtools_observer_registration, + webview_attributes, + }], + window_event_listeners: Arc::new(Mutex::new(HashMap::new())), + webview_event_listeners: Arc::new(Mutex::new(HashMap::new())), + }, ); } - } + }); - context.windows.borrow_mut().insert( - window_id, - AppWindow { - label, - window: crate::AppWindowKind::BrowserWindow, - force_close: force_close.clone(), - attributes: attributes.clone(), - webviews: vec![AppWebview { - webview_id, - browser_id: Arc::new(RefCell::new(browser_id_val)), - label: webview_label, - inner: browser, - bounds: Arc::new(Mutex::new(None)), - devtools_enabled, - uri_scheme_protocols: Arc::new(uri_scheme_protocols), - initialization_scripts, - devtools_protocol_handlers, - devtools_observer_registration, - webview_attributes: Arc::new(RefCell::new(webview_attributes)), - }], - window_event_listeners: Arc::new(Mutex::new(HashMap::new())), - webview_event_listeners: Arc::new(Mutex::new(HashMap::new())), - }, + request_context_from_webview_attributes( + context, + &webview_attributes.borrow(), + &custom_schemes, + &custom_protocol_scheme, + on_initialized, ); + + wait_for_deferred_init(&init_done); } pub(crate) fn create_window( @@ -3414,6 +4132,7 @@ pub(crate) fn create_window( attributes.clone(), RefCell::new(Default::default()), RefCell::new(Default::default()), + RefCell::new(1.0), RefCell::new(false), context.clone(), ); @@ -3472,12 +4191,75 @@ wrap_task! { } } +wrap_task! { + struct WebviewEventTask { + context: Context, + window_id: WindowId, + webview_id: u32, + event: WebviewEvent, + } + + impl Task { + fn execute(&self) { + send_webview_event( + &self.context, + self.window_id, + self.webview_id, + self.event.clone(), + ); + } + } +} + +wrap_task! { + struct DragDropScriptEventTask { + context: Context, + window_id: WindowId, + webview_id: u32, + target: DragDropEventTarget, + drag_drop_state: Arc>, + event: DragDropScriptEvent, + } + + impl Task { + fn execute(&self) { + handle_drag_drop_script_event( + &self.context, + self.window_id, + self.webview_id, + self.target, + self.drag_drop_state.clone(), + self.event.clone(), + ); + } + } +} + #[cfg(target_os = "macos")] fn send_message_task(context: &Context, message: Message) { let mut task = SendMessageTask::new(context.clone(), Arc::new(RefCell::new(message))); cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); } +fn post_drag_drop_script_event( + context: Context, + window_id: WindowId, + webview_id: u32, + target: DragDropEventTarget, + drag_drop_state: Arc>, + event: DragDropScriptEvent, +) { + let mut task = DragDropScriptEventTask::new( + context, + window_id, + webview_id, + target, + drag_drop_state, + event, + ); + cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); +} + fn send_window_event( window_id: WindowId, windows: &Arc>>, @@ -3500,32 +4282,156 @@ fn send_window_event( drop(windows_ref); - { + in_callback(|| { let listeners = window_event_listeners.lock().unwrap(); let handlers: Vec<_> = listeners.values().collect(); for handler in handlers.iter() { handler(&event); } + }); + + in_callback(|| (callback.borrow())(RunEvent::WindowEvent { label, event })); + } +} + +fn send_webview_event( + context: &Context, + window_id: WindowId, + webview_id: u32, + event: WebviewEvent, +) { + let Ok(windows_ref) = context.windows.try_borrow() else { + let mut task = WebviewEventTask::new(context.clone(), window_id, webview_id, event.clone()); + cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); + return; + }; + + let Some(w) = windows_ref.get(&window_id) else { + return; + }; + + let listeners = w.webview_event_listeners.clone(); + drop(windows_ref); + + let Some(webview_listeners) = listeners.lock().unwrap().get(&webview_id).cloned() else { + return; + }; + + in_callback(|| { + let listeners = webview_listeners.lock().unwrap(); + let handlers: Vec<_> = listeners.values().collect(); + for handler in handlers.iter() { + handler(&event); } + }); +} - (callback.borrow())(RunEvent::WindowEvent { label, event }); +fn send_drag_drop_event( + context: &Context, + window_id: WindowId, + webview_id: u32, + target: DragDropEventTarget, + event: DragDropEvent, +) { + match target { + DragDropEventTarget::Window => send_window_event( + window_id, + &context.windows, + &context.callback, + WindowEvent::DragDrop(event), + ), + DragDropEventTarget::Webview => send_webview_event( + context, + window_id, + webview_id, + WebviewEvent::DragDrop(event), + ), } } -fn on_close_requested( +fn handle_drag_drop_script_event( + context: &Context, window_id: WindowId, - windows: &Arc>>, - callback: &RunEventCallback, + webview_id: u32, + target: DragDropEventTarget, + drag_drop_state: Arc>, + script_event: DragDropScriptEvent, ) { + let position = PhysicalPosition::new(script_event.x, script_event.y); + let event = { + let mut state = drag_drop_state.lock().unwrap(); + if !state.native_entered { + return; + } + + match script_event.kind.as_str() { + "enter" => { + if state.entered { + return; + } + + let Some(paths) = state.paths.clone() else { + return; + }; + state.entered = true; + Some(DragDropEvent::Enter { paths, position }) + } + "over" => { + if state.entered { + Some(DragDropEvent::Over { position }) + } else { + None + } + } + "drop" => { + let paths = state.entered.then(|| state.paths.take()).flatten(); + state.entered = false; + state.native_entered = false; + paths.map(|paths| DragDropEvent::Drop { paths, position }) + } + "leave" => { + state.native_entered = false; + state.paths = None; + + if state.entered { + state.entered = false; + Some(DragDropEvent::Leave) + } else { + None + } + } + _ => None, + } + }; + + if let Some(event) = event { + send_drag_drop_event(context, window_id, webview_id, target, event); + } +} + +fn on_close_requested(window_id: WindowId, context: &Context) { + // Skip `CloseRequested` while tearing down — the embedder has already been + // told `ExitRequested -> Exit`. We still need to drive the close so that + // CEF can run its `OnBeforeClose` lifecycle. + if context.is_shutting_down.load(Ordering::SeqCst) { + on_window_close(window_id, &context.windows); + return; + } + let (tx, rx) = channel(); let event = WindowEvent::CloseRequested { signal_tx: tx }; - send_window_event(window_id, windows, callback, event.clone()); + send_window_event( + window_id, + &context.windows, + &context.callback, + event.clone(), + ); let prevent = rx.try_recv().unwrap_or_default(); if !prevent { - on_window_close(window_id, windows); + on_window_close(window_id, &context.windows); } } @@ -3537,8 +4443,32 @@ fn collect_hosts(webviews: &[AppWebview]) -> Vec { .collect() } -/// Force-close all windows, triggering the normal CEF lifecycle: -/// force_close → can_close → close_window_browsers → on_before_close → on_window_destroyed. +/// Apply the assistive-technology accessibility state to every live browser +/// host — the runtime-wide equivalent of cefclient's `enableAccessibility:`, +/// which only toggled the active browser. +#[cfg(target_os = "macos")] +pub fn set_browsers_accessibility_state(context: &Context, enabled: bool) { + let state = if enabled { + State::ENABLED + } else { + State::DISABLED + }; + for app_window in context.windows.borrow().values() { + for host in collect_hosts(&app_window.webviews) { + host.set_accessibility_state(state); + } + } +} + +/// Tear-down equivalent of cefclient's `RootWindowManager::CloseAllWindows`: +/// drives every remaining window through the normal CEF lifecycle so each +/// browser sees `OnBeforeClose`. +/// +/// Each call goes `force_close → can_close → close_window_browsers → +/// on_before_close → on_window_destroyed`. While `Context::is_shutting_down` +/// is set, those callbacks suppress their public events so the embedder only +/// sees the `ExitRequested -> Exit` pair we already emitted for the direct +/// exit. pub fn close_all_windows(windows: &Arc>>) { let window_ids: Vec<_> = windows.borrow().keys().copied().collect(); for window_id in window_ids { @@ -3592,6 +4522,8 @@ fn on_window_destroyed(window_id: WindowId, context: &Context) return; } + let is_shutting_down = context.is_shutting_down.load(Ordering::SeqCst); + let event = WindowEvent::Destroyed; send_window_event(window_id, &context.windows, &context.callback, event); @@ -3613,7 +4545,11 @@ fn on_window_destroyed(window_id: WindowId, context: &Context) drop(removed_window); let is_empty = context.windows.borrow().is_empty(); - if is_empty { + // Window-close exit path: only emit the terminal `ExitRequested -> Exit` + // pair when this is the last window being destroyed naturally. If we're + // already shutting down (direct-exit tear-down or a previously approved + // exit) the events have already been delivered. + if is_empty && !is_shutting_down { let (tx, rx) = channel(); (context.callback.borrow())(RunEvent::ExitRequested { code: None, tx }); @@ -3621,6 +4557,7 @@ fn on_window_destroyed(window_id: WindowId, context: &Context) let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); if !should_prevent { + context.is_shutting_down.store(true, Ordering::SeqCst); (context.callback.borrow())(RunEvent::Exit); } } @@ -3639,7 +4576,7 @@ pub(crate) fn create_webview( mut webview_attributes, platform_specific_attributes, uri_scheme_protocols, - ipc_handler: _, + ipc_handler, navigation_handler, new_window_handler, document_title_changed_handler, @@ -3648,6 +4585,9 @@ pub(crate) fn create_webview( web_resource_request_handler: _, mut on_page_load_handler, download_handler, + // TODO + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate_handler: _, } = pending; let address_changed_handler = address_changed_handler @@ -3666,16 +4606,15 @@ pub(crate) fn create_webview( } }; - let initialization_scripts = std::mem::take(&mut webview_attributes.initialization_scripts) - .into_iter() - .map(CefInitScript::new) - .collect::>(); - let initialization_scripts = Arc::new(initialization_scripts); + let drag_drop_handler_enabled = webview_attributes.drag_drop_handler_enabled; + let initialization_scripts = + initialization_scripts_from_webview_attributes(&mut webview_attributes); let on_page_load_handler = on_page_load_handler.take().map(Arc::from); let document_title_changed_handler = document_title_changed_handler.map(Arc::from); let navigation_handler = navigation_handler.map(Arc::from); let new_window_handler = new_window_handler.map(Arc::from); + let ipc_handler: Option>> = ipc_handler.map(Arc::from); let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) && webview_attributes.devtools.unwrap_or(true); @@ -3684,21 +4623,33 @@ pub(crate) fn create_webview( "https" } else { "http" - }; + } + .to_string(); let custom_schemes = uri_scheme_protocols.keys().cloned().collect::>(); - let custom_scheme_domain_names: Vec = custom_schemes + let custom_scheme_domain_names = custom_schemes .iter() .map(|scheme| format!("{scheme}.localhost")) - .collect(); + .collect::>(); let initial_url = url.clone(); - let url = CefString::from(url.as_str()); + let url = CefString::from(INITIAL_LOAD_URL); + let drag_drop_state = Arc::new(Mutex::new(DragDropState::default())); + let drag_drop_event_target = if kind == WebviewKind::WindowContent { + DragDropEventTarget::Window + } else { + DragDropEventTarget::Webview + }; - let mut client = BrowserClient::new( + let client = BrowserClient::new( WindowKind::Tauri, window_id, - initialization_scripts.clone(), + webview_id, + label.clone(), + drag_drop_event_target, + drag_drop_handler_enabled, + drag_drop_state, + ipc_handler, on_page_load_handler, document_title_changed_handler, navigation_handler, @@ -3706,10 +4657,9 @@ pub(crate) fn create_webview( new_window_handler, download_handler, devtools_enabled, - custom_scheme_domain_names.clone(), - custom_protocol_scheme.to_string(), context.clone(), - Some(initial_url.clone()), + runtime_context(context), + None, ); let uri_scheme_protocols: HashMap>> = @@ -3718,33 +4668,6 @@ pub(crate) fn create_webview( .map(|(k, v)| (k, Arc::new(v))) .collect(); - let mut request_context = request_context_from_webview_attributes( - context, - &webview_attributes, - &custom_schemes, - custom_protocol_scheme, - &initialization_scripts, - ); - let window_theme = context - .windows - .borrow() - .get(&window_id) - .and_then(|w| w.attributes.borrow().theme); - apply_request_context_theme_scheme(request_context.as_ref(), window_theme); - - let browser_settings = browser_settings_from_webview_attributes(&webview_attributes); - - let bounds = webview_attributes.bounds.map(|bounds| { - let device_scale_factor = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - rect_to_cef(bounds, device_scale_factor) - }); - - let window_handle = window.window_handle(); - let runtime_style = platform_specific_attributes .iter() .map(|attr| match attr { @@ -3756,155 +4679,226 @@ pub(crate) fn create_webview( } else { CefRuntimeStyle::Chrome }); - let cef_runtime_style: RuntimeStyle = match runtime_style { CefRuntimeStyle::Alloy => cef_runtime_style_t::CEF_RUNTIME_STYLE_ALLOY.into(), CefRuntimeStyle::Chrome => cef_runtime_style_t::CEF_RUNTIME_STYLE_CHROME.into(), }; - if kind == WebviewKind::WindowChild { - #[cfg(target_os = "macos")] - let window_handle = ensure_valid_content_view(window_handle); + let window_theme = context + .windows + .borrow() + .get(&window_id) + .and_then(|w| w.attributes.borrow().theme); - let mut window_info = cef::WindowInfo::default().set_as_child( - window_handle, - bounds.as_ref().unwrap_or(&cef::Rect::default()), - ); - window_info.runtime_style = cef_runtime_style; - - let Some(browser_host) = browser_host_create_browser_sync( - Some(&window_info), - Some(&mut client), - Some(&url), - Some(&browser_settings), - Option::<&mut DictionaryValue>::None, - request_context.as_mut(), - ) else { - eprintln!("Failed to create browser"); - return; - }; + let webview_attributes = Arc::new(RefCell::new(webview_attributes)); + + // Browser creation is deferred to this continuation, which runs on the CEF + // UI thread once the request context's underlying Chromium `Profile` is + // initialized. Calling `browser_view_create` / + // `browser_host_create_browser_sync` synchronously after + // `request_context_create_context` would fail + // `CefRequestContextImpl::VerifyBrowserContext()` whenever the per-webview + // cache_path triggers `ChromeBrowserContext`'s asynchronous + // `CreateProfileAsync` branch (i.e., any non-default `data_directory`). + // + // We then pump the message loop after returning from the call below until + // the continuation has finished, so that the function appears synchronous + // to the runtime: any operation the caller queues against the new webview + // (`open_devtools`, `on_dev_tools_protocol`, ...) is guaranteed to find it. + let (init_done, on_initialized) = deferred_init_continuation({ + let context = context.clone(); + let webview_attributes = webview_attributes.clone(); + let initialization_scripts = initialization_scripts.clone(); + let custom_protocol_scheme = custom_protocol_scheme.clone(); + let mut client = client; + move |mut request_context| { + apply_request_context_theme_scheme(request_context.as_ref(), window_theme); + + let browser_settings = browser_settings_from_webview_attributes(&webview_attributes.borrow()); + + let bounds = webview_attributes.borrow().bounds.map(|b| { + let device_scale_factor = window + .display() + .map(|d| d.device_scale_factor() as f64) + .unwrap_or(1.0); + rect_to_cef(b, device_scale_factor) + }); - // On Windows, set the browser window to be topmost to esnure correct z-order - #[cfg(windows)] - set_browser_on_top(&browser_host); + let window_handle = window.window_handle(); - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< - Arc, - >::new())); - let devtools_observer_registration = Arc::new(Mutex::new(None)); + if kind == WebviewKind::WindowChild { + #[cfg(target_os = "macos")] + let window_handle = ensure_valid_content_view(window_handle); - let browser = CefWebview::Browser(browser_host); + let mut window_info = cef::WindowInfo::default().set_as_child( + window_handle, + bounds.as_ref().unwrap_or(&cef::Rect::default()), + ); + window_info.runtime_style = cef_runtime_style; - browser.set_bounds(bounds.as_ref()); + let Some(browser_host) = browser_host_create_browser_sync( + Some(&window_info), + Some(&mut client), + Some(&url), + Some(&browser_settings), + Option::<&mut DictionaryValue>::None, + request_context.as_mut(), + ) else { + eprintln!("Failed to create browser"); + return; + }; + let browser_id_val = browser_host.identifier(); + { + let mut registry = context.scheme_handler_registry.lock().unwrap(); + for (scheme, handler) in &uri_scheme_protocols { + registry.insert( + (browser_id_val, scheme.clone()), + ( + label.clone(), + handler.clone(), + initialization_scripts.clone(), + ), + ); + } + } - // On Linux, explicitly set parent after creation as set_as_child may not work correctly - #[cfg(target_os = "linux")] - { - // Try to set parent - if window handle isn't available yet, this will be a no-op - // but the browser should become visible once the handle is available - browser.set_parent(&window); - // Ensure browser is visible after setting parent - browser.set_visible(1); - // Set bounds again after reparenting to ensure correct size - browser.set_bounds(bounds.as_ref()); - } + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< + Arc, + >::new())); + let pending_initial_loads = Arc::new(Mutex::new(HashMap::new())); + let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser_host, + devtools_protocol_handlers.clone(), + pending_initial_loads.clone(), + ))); + + load_initial_url_after_registering_initialization_scripts( + &browser_host, + &initialization_scripts, + &custom_protocol_scheme, + &custom_scheme_domain_names, + &initial_url, + &pending_initial_loads, + ); - let initial_bounds_ratio = if webview_attributes.auto_resize { - Some(webview_bounds_ratio(&window, bounds.clone(), &browser)) - } else { - None - }; + // On Windows, set the browser window to be topmost to esnure correct z-order + #[cfg(windows)] + set_browser_on_top(&browser_host); - let browser_id_val = browser.browser_id(); - { - let mut registry = context.scheme_handler_registry.lock().unwrap(); - for (scheme, handler) in &uri_scheme_protocols { - registry.insert( - (browser_id_val, scheme.clone()), - ( - label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), + let browser = CefWebview::Browser(browser_host); + + browser.set_bounds(bounds.as_ref()); + + // On Linux, explicitly set parent after creation as set_as_child may not work correctly + #[cfg(target_os = "linux")] + { + // Try to set parent - if window handle isn't available yet, this will be a no-op + // but the browser should become visible once the handle is available + browser.set_parent(&window); + // Ensure browser is visible after setting parent + browser.set_visible(1); + // Set bounds again after reparenting to ensure correct size + browser.set_bounds(bounds.as_ref()); + } + + let auto_resize = webview_attributes.borrow().auto_resize; + let initial_bounds_ratio = if auto_resize { + Some(webview_bounds_ratio(&window, bounds.clone(), &browser)) + } else { + None + }; + + context + .windows + .borrow_mut() + .get_mut(&window_id) + .unwrap() + .webviews + .push(AppWebview { + label, + webview_id, + browser_id: Arc::new(RefCell::new(browser_id_val)), + bounds: Arc::new(Mutex::new(initial_bounds_ratio)), + inner: browser, + devtools_enabled, + uri_scheme_protocols: Arc::new(uri_scheme_protocols), + initialization_scripts, + devtools_protocol_handlers, + devtools_observer_registration, + webview_attributes, + }); + } else { + let browser_id = Arc::new(RefCell::new(0)); + let uri_scheme_protocols = Arc::new(uri_scheme_protocols); + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< + Arc, + >::new())); + let devtools_observer_registration = Arc::new(Mutex::new(None)); + let pending_initial_loads = Arc::new(Mutex::new(HashMap::new())); + + #[allow(clippy::unnecessary_find_map)] + let mut browser_view_delegate = BrowserViewDelegateImpl::new( + browser_id.clone(), + runtime_style, + context.scheme_handler_registry.clone(), + label.clone(), + uri_scheme_protocols.clone(), + initialization_scripts.clone(), + custom_protocol_scheme.clone(), + custom_scheme_domain_names.clone(), + initial_url.clone(), + pending_initial_loads, + devtools_protocol_handlers.clone(), + devtools_observer_registration.clone(), + webview_attributes.clone(), ); - } - } - context - .windows - .borrow_mut() - .get_mut(&window_id) - .unwrap() - .webviews - .push(AppWebview { - label, - webview_id, - browser_id: Arc::new(RefCell::new(browser_id_val)), - bounds: Arc::new(Mutex::new(initial_bounds_ratio)), - inner: browser, - devtools_enabled, - uri_scheme_protocols: Arc::new(uri_scheme_protocols), - initialization_scripts, - devtools_protocol_handlers, - devtools_observer_registration, - webview_attributes: Arc::new(RefCell::new(webview_attributes)), - }); - } else { - let browser_id = Arc::new(RefCell::new(0)); - let uri_scheme_protocols = Arc::new(uri_scheme_protocols); - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< - Arc, - >::new())); - let devtools_observer_registration = Arc::new(Mutex::new(None)); - let webview_attributes = Arc::new(RefCell::new(webview_attributes)); - - #[allow(clippy::unnecessary_find_map)] - let mut browser_view_delegate = BrowserViewDelegateImpl::new( - browser_id.clone(), - runtime_style, - context.scheme_handler_registry.clone(), - label.clone(), - uri_scheme_protocols.clone(), - initialization_scripts.clone(), - devtools_protocol_handlers.clone(), - devtools_observer_registration.clone(), - webview_attributes.clone(), - ); + let browser_view = browser_view_create( + Some(&mut client), + Some(&url), + Some(&browser_settings), + Option::<&mut DictionaryValue>::None, + request_context.as_mut(), + Some(&mut browser_view_delegate), + ) + .expect("Failed to create browser view"); - let browser_view = browser_view_create( - Some(&mut client), - Some(&url), - Some(&browser_settings), - Option::<&mut DictionaryValue>::None, - request_context.as_mut(), - Some(&mut browser_view_delegate), - ) - .expect("Failed to create browser view"); + let browser_webview = CefWebview::BrowserView(browser_view.clone()); - let browser_webview = CefWebview::BrowserView(browser_view.clone()); + window.add_child_view(Some(&mut View::from(&browser_view))); - window.add_child_view(Some(&mut View::from(&browser_view))); + context + .windows + .borrow_mut() + .get_mut(&window_id) + .unwrap() + .webviews + .push(AppWebview { + inner: browser_webview, + label, + webview_id, + browser_id, + bounds: Arc::new(Mutex::new(None)), + devtools_enabled, + uri_scheme_protocols, + initialization_scripts, + devtools_protocol_handlers, + devtools_observer_registration, + webview_attributes, + }); + } + } + }); - context - .windows - .borrow_mut() - .get_mut(&window_id) - .unwrap() - .webviews - .push(AppWebview { - inner: browser_webview, - label, - webview_id, - browser_id, - bounds: Arc::new(Mutex::new(None)), - devtools_enabled, - uri_scheme_protocols, - initialization_scripts, - devtools_protocol_handlers, - devtools_observer_registration, - webview_attributes, - }); - } + request_context_from_webview_attributes( + context, + &webview_attributes.borrow(), + &custom_schemes, + &custom_protocol_scheme, + on_initialized, + ); + + wait_for_deferred_init(&init_done); } #[cfg(windows)] @@ -4011,24 +5005,248 @@ fn browser_settings_from_webview_attributes( } } +/// Resolves a CEF-compatible cache path for a per-webview request context. +/// +/// CEF requires `RequestContextSettings.cache_path` to be either empty (which +/// puts the context in incognito mode) or an absolute path that is equal to, +/// or a child directory of, `Settings.root_cache_path` (which defaults to +/// `Settings.cache_path` when not set explicitly). Any value outside of that +/// root makes `request_context_create_context` (and downstream browser +/// creation) fail. +/// +/// To support an arbitrary [`WebviewAttributes::data_directory`] while +/// honoring this constraint we: +/// +/// * use the requested path directly when it already lives under the global +/// cache root, so callers that opt in to a path under the app cache get the +/// exact location they asked for; +/// * join relative paths without parent (`..`) components onto the root cache +/// path (typical short labels); and +/// * otherwise derive a stable direct child folder under `/` from +/// the requested path, preserving isolation between webviews. Distinct +/// `data_directory` values produce distinct profiles, and the same value +/// maps to the same on-disk profile across runs. +fn resolve_request_context_cache_path(global_cache_path: &Path, data_directory: &Path) -> PathBuf { + if data_directory.is_absolute() { + if data_directory.starts_with(global_cache_path) { + return data_directory.to_path_buf(); + } else { + log::warn!( + "data directory is not a child of the global cache path, we will derive a profile hash from it" + ); + } + } else if !data_directory + .components() + .any(|c| matches!(c, Component::ParentDir)) + { + return global_cache_path.join(data_directory); + } else { + log::warn!( + "data directory is a relative path with parent components, we will derive a profile hash from it" + ); + } + + let mut hasher = Sha256::new(); + hasher.update(data_directory.as_os_str().as_encoded_bytes()); + let hash = hasher.finalize(); + let suffix = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash[..16]); + let path = global_cache_path.join(format!("Profile-{suffix}")); + log::info!( + "derived profile hash from data directory: {suffix}, cache path: {}", + path.display() + ); + path +} + +/// Continuation invoked on the CEF UI thread once the request context's +/// underlying browser context has finished asynchronous initialization. +/// +/// Receives a fresh handle to the same [`RequestContext`] that was created in +/// [`request_context_from_webview_attributes`], so the continuation can pass +/// it to `browser_view_create` / `browser_host_create_browser_sync` knowing +/// that `VerifyBrowserContext()` will succeed. +type RequestContextInitContinuation = Box) + 'static>; + +/// Wraps a deferred-init continuation so that it always flips a shared +/// completion flag when it exits, regardless of how it exits (normal return, +/// early `return` on browser-create failure, or panic). +/// +/// Returns the completion flag plus the wrapped continuation. +fn deferred_init_continuation(work: F) -> (Arc, RequestContextInitContinuation) +where + F: FnOnce(Option) + 'static, +{ + struct Guard(Arc); + impl Drop for Guard { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + let flag = Arc::new(AtomicBool::new(false)); + let guard = Guard(flag.clone()); + let wrapped: RequestContextInitContinuation = Box::new(move |request_context| { + let _guard = guard; + work(request_context); + }); + (flag, wrapped) +} + +/// Block the calling thread until `flag` is `true`. +/// +/// Browser creation goes through `RequestContextHandler::on_request_context_initialized`, +/// which CEF always dispatches via `CEF_POST_TASK(CEF_UIT, ...)`. Tauri runs +/// CEF with an external message pump (see `cef::do_message_loop_work` in the +/// runtime's main loop), so the only way for that posted task to actually +/// execute is for someone on the CEF UI thread to keep pumping the loop. +/// +/// Two cases: +/// +/// 1. We're on the CEF UI thread (typical: app setup, [`SendMessageTask`] +/// dispatched messages, or inside a CEF callback like +/// `LifeSpanHandler::on_after_created` / +/// `RequestHandler::on_open_url_from_tab`). Pump the message loop ourselves +/// so the `OnRequestContextInitialized` task can run. +/// +/// We must enable nestable tasks for the duration of the pump because we +/// may already be running inside another CEF task; without +/// `CefSetNestableTasksAllowed(true)` Chromium's `RunLoop::RunUntilIdle` +/// refuses to dispatch any task to the UI thread, the deferred init never +/// fires, and we'd spin here forever. +/// +/// 2. We're on some other thread (e.g. a tokio IPC handler that called the +/// Tauri API directly without going through [`RuntimeContext::post_message`]). +/// The CEF UI thread is running its own pump and will pick up our queued +/// init task on its own; we just block here on a sleep loop until the flag +/// flips. We can't call `do_message_loop_work` from this thread - it +/// asserts on the init thread. +/// +/// Spinning here keeps `create_webview` / `create_browser_window` synchronous +/// from the caller's perspective: the function does not return until the +/// browser exists in `context.windows`, so any subsequent dispatcher call +/// (e.g. `webview.open_devtools()`, `webview.on_dev_tools_protocol(...)`) +/// can find the webview. +fn wait_for_deferred_init(flag: &Arc) { + let on_ui_thread = cef::currently_on(cef::sys::cef_thread_id_t::TID_UI.into()) != 0; + + if on_ui_thread { + let _allow = AllowNestableTasks::enter(); + while !flag.load(Ordering::SeqCst) { + cef::do_message_loop_work(); + } + } else { + while !flag.load(Ordering::SeqCst) { + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } +} + +/// RAII guard that scopes `CefSetNestableTasksAllowed(true)` for the current +/// CEF UI-thread call. +/// +/// CEF requires balanced enable/disable calls and explicitly forbids +/// reentrancy at the C++ level (`CHECK(allowed != has_value())`). The guard +/// uses a thread-local depth counter so only the outermost +/// [`wait_for_deferred_init`] on this thread toggles the flag, which makes +/// nesting (e.g. an `on_initialized` continuation that creates another +/// webview) safe. +struct AllowNestableTasks; + +impl AllowNestableTasks { + fn enter() -> Self { + NESTABLE_TASKS_DEPTH.with(|depth| { + let current = depth.get(); + if current == 0 { + cef::set_nestable_tasks_allowed(1); + } + depth.set(current + 1); + }); + Self + } +} + +impl Drop for AllowNestableTasks { + fn drop(&mut self) { + NESTABLE_TASKS_DEPTH.with(|depth| { + let current = depth.get(); + depth.set(current - 1); + if current == 1 { + cef::set_nestable_tasks_allowed(0); + } + }); + } +} + +thread_local! { + static NESTABLE_TASKS_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +wrap_request_context_handler! { + struct WebviewRequestContextHandler { + on_initialized: Arc>>, + } + + impl RequestContextHandler { + fn on_request_context_initialized(&self, request_context: Option<&mut RequestContext>) { + let Some(callback) = self.on_initialized.lock().unwrap().take() else { + return; + }; + let request_context = request_context.map(|rc| rc.clone()); + callback(request_context); + } + } +} + +/// Creates a per-webview [`RequestContext`], registers Tauri's custom URI +/// scheme handler factories on it, and arranges for `on_initialized` to fire +/// once the underlying Chromium `Profile` is fully created. +/// +/// CEF only synchronously initializes the request context when its `cache_path` +/// equals `Settings.root_cache_path` (it then reuses the global "Default" +/// profile via `GetPrimaryUserProfile()`) or when the cache_path is empty +/// (off-the-record profile). Any other path (notably the per-`data_directory` +/// case used by Tauri) takes `ChromeBrowserContext::InitializeAsync`'s +/// `CreateProfileAsync` branch which finishes asynchronously. Calling +/// `browser_view_create` / `browser_host_create_browser_sync` synchronously +/// after `request_context_create_context` would then fail +/// `CefRequestContextImpl::VerifyBrowserContext()` and return a null browser. +/// +/// Routing browser creation through `on_initialized` keeps a single code path +/// for every cache_path layout: CEF always dispatches the callback through +/// `CEF_POST_TASK(CEF_UIT, ...)`, so even the synchronous-init cases are +/// handled by the same continuation. +/// +/// Scheme handler factories are registered here, synchronously after +/// `request_context_create_context` returns, and *before* the +/// `OnRequestContextInitialized` task that drives browser creation is +/// dispatched. `RegisterSchemeHandlerFactory` internally queues its work +/// behind the request context's initialization (`StoreOrTriggerInitCallback` +/// when the browser context is not yet initialized, or an immediate UI -> IO +/// hop otherwise), so by the time the browser finally issues its first +/// navigation against any of these schemes the factories have been wired up +/// on the IO thread. fn request_context_from_webview_attributes( context: &Context, webview_attributes: &WebviewAttributes, custom_schemes: &[String], custom_protocol_scheme: &str, - _initialization_scripts: &[CefInitScript], + on_initialized: RequestContextInitContinuation, ) -> Option { - let global_context = - request_context_get_global_context().expect("Failed to get global request context"); - let cache_path: CefStringUtf16 = if webview_attributes.incognito { CefStringUtf16::from("") - } else if let Some(_data_directory) = &webview_attributes.data_directory { - // TODO: setting a custom data directory must be a child of the root data directory, but it returns None on browser_view_create - eprintln!("data directory is not yet implemented"); - (&global_context.cache_path()).into() - // CefStringUtf16::from(data_directory.to_string_lossy().as_ref()) + } else if let Some(data_directory) = &webview_attributes.data_directory { + let resolved = resolve_request_context_cache_path(&context.cache_path, data_directory); + if let Err(error) = std::fs::create_dir_all(&resolved) { + log::error!( + "failed to create request context cache directory {}: {error}", + resolved.display() + ); + } + CefStringUtf16::from(resolved.to_string_lossy().as_ref()) } else { + let global_context = + request_context_get_global_context().expect("Failed to get global request context"); + // context.cache_path does not work here - global_context.cache_path() returns the proper profile path (&global_context.cache_path()).into() }; @@ -4037,13 +5255,28 @@ fn request_context_from_webview_attributes( ..Default::default() }; - let request_context = request_context_create_context( - Some(&request_context_settings), - Option::<&mut RequestContextHandler>::None, - ); - if let Some(request_context) = &request_context { + // Holds a strong reference to the `RequestContext` until the + // `on_request_context_initialized` callback fires. CEF keeps the underlying + // C++ `CefRequestContextImpl` alive during async profile creation through + // its own bound callbacks, but holding an explicit reference here guarantees + // we don't race with reference-count releases on shutdown paths. + let rc_holder: Arc>> = Arc::new(Mutex::new(None)); + let wrapped_callback: RequestContextInitContinuation = Box::new({ + let rc_holder = rc_holder.clone(); + move |rc| { + on_initialized(rc); + let _released = rc_holder.lock().unwrap().take(); + } + }); + + let mut handler = WebviewRequestContextHandler::new(Arc::new(Mutex::new(Some(wrapped_callback)))); + let request_context = + request_context_create_context(Some(&request_context_settings), Some(&mut handler)); + *rc_holder.lock().unwrap() = request_context.clone(); + + if let Some(rc) = request_context.as_ref() { for custom_scheme in custom_schemes { - request_context.register_scheme_handler_factory( + rc.register_scheme_handler_factory( Some(&custom_protocol_scheme.into()), Some(&format!("{custom_scheme}.localhost").as_str().into()), Some(&mut request_handler::UriSchemeHandlerFactory::new( diff --git a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs index 107264473b61..87a278719975 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs @@ -5,7 +5,7 @@ use std::{ borrow::Cow, io::{Cursor, Read}, - sync::Arc, + sync::{Arc, Mutex}, }; use cef::{rc::*, *}; @@ -13,17 +13,22 @@ use dioxus_debug_cell::RefCell; use html5ever::{LocalName, interface::QualName, namespace_url, ns}; use http::{ HeaderMap, HeaderName, HeaderValue, - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE}, + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, ORIGIN}, }; use kuchiki::NodeRef; -use tauri_runtime::webview::UriSchemeProtocolHandler; +use tauri_runtime::{UserEvent, webview::UriSchemeProtocolHandler, window::WindowId}; use tauri_utils::{ config::{Csp, CspDirectiveSources}, html::{parse as parse_html, serialize_node}, }; use url::Url; -use super::CefInitScript; +use crate::cef_impl::INITIAL_LOAD_URL; + +use super::{ + CefInitScript, Context, DRAG_DROP_BRIDGE_PATH, DragDropEventTarget, DragDropScriptEvent, + DragDropState, post_drag_drop_script_event, +}; type HttpResponse = Arc>>>>>; @@ -35,8 +40,6 @@ fn csp_inject_initialization_scripts_hashes( return existing_csp; } - // For custom schemes, include ALL script hashes (we inject all scripts into HTML) - // This matches the HTML injection behavior in inject_scripts_into_html_body let script_hashes: Vec = initialization_scripts .iter() .map(|s| s.hash.clone()) @@ -46,33 +49,26 @@ fn csp_inject_initialization_scripts_hashes( return existing_csp; } - // Parse CSP using tauri-utils let mut csp_map: std::collections::HashMap = Csp::Policy(existing_csp.to_string()).into(); - // Update or create script-src directive with script hashes let script_src = csp_map .entry("script-src".to_string()) .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); - // Extend with script hashes script_src.extend(script_hashes); - // Convert back to CSP string Csp::DirectiveMap(csp_map).to_string() } -/// Helper function to inject initialization scripts into HTML body fn inject_scripts_into_html_body( body: &[u8], initialization_scripts: &[CefInitScript], ) -> Option> { - // Check if body is valid UTF-8 HTML let Ok(body_str) = std::str::from_utf8(body) else { return None; }; - // Parse HTML and inject scripts let document = parse_html(body_str.to_string()); let head = if let Ok(ref head_node) = document.select_first("head") { @@ -86,20 +82,23 @@ fn inject_scripts_into_html_body( head_node }; - // Inject initialization scripts (for custom schemes, inject all scripts) for init_script in initialization_scripts.iter().rev() { let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); script_el.append(NodeRef::new_text(init_script.script.script.as_str())); head.prepend(script_el); } - // Serialize the modified HTML Some(serialize_node(&document)) } wrap_resource_request_handler! { - pub struct WebResourceRequestHandler { - initialization_scripts: Arc>, + pub struct WebResourceRequestHandler { + context: Context, + window_id: WindowId, + webview_id: u32, + drag_drop_event_target: DragDropEventTarget, + drag_drop_handler_enabled: bool, + drag_drop_state: Arc>, } impl ResourceRequestHandler { @@ -109,18 +108,49 @@ wrap_resource_request_handler! { &self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, - _request: Option<&mut Request>, + request: Option<&mut Request>, _callback: Option<&mut Callback>, ) -> ReturnValue { + if self.drag_drop_handler_enabled + && let Some(request) = request + { + let url = CefString::from(&request.url()).to_string(); + if let Ok(url) = Url::parse(&url) + && url.path() == DRAG_DROP_BRIDGE_PATH + { + if let Some(payload) = url.query_pairs().find_map(|(key, value)| { + (key == "payload").then(|| value.into_owned()) + }) + && let Ok(event) = serde_json::from_str::(&payload) + { + post_drag_drop_script_event( + self.context.clone(), + self.window_id, + self.webview_id, + self.drag_drop_event_target, + self.drag_drop_state.clone(), + event, + ); + } + + return sys::cef_return_value_t::RV_CANCEL.into(); + } + } + sys::cef_return_value_t::RV_CONTINUE.into() } } } wrap_request_handler! { - pub struct WebRequestHandler { - initialization_scripts: Arc>, + pub struct WebRequestHandler { navigation_handler: Option>, + context: Context, + window_id: WindowId, + webview_id: u32, + drag_drop_event_target: DragDropEventTarget, + drag_drop_handler_enabled: bool, + drag_drop_state: Arc>, } impl RequestHandler { @@ -139,17 +169,24 @@ wrap_request_handler! { if frame.is_main() == 0 { return 0; } - let Some(handler) = &self.navigation_handler else { - return 0; - }; let Some(request) = request else { return 0; }; let url_str = CefString::from(&request.url()).to_string(); + + if url_str == INITIAL_LOAD_URL { + return 0; + } + let Ok(url) = url::Url::parse(&url_str) else { return 0; }; + + let Some(handler) = &self.navigation_handler else { + return 0; + }; + let should_navigate = handler(&url); if should_navigate { 0 @@ -169,7 +206,12 @@ wrap_request_handler! { _disable_default_handling: Option<&mut ::std::os::raw::c_int>, ) -> Option { Some(WebResourceRequestHandler::new( - self.initialization_scripts.clone(), + self.context.clone(), + self.window_id, + self.webview_id, + self.drag_drop_event_target, + self.drag_drop_handler_enabled, + self.drag_drop_state.clone(), )) } } @@ -180,6 +222,14 @@ wrap_resource_handler! { webview_label: String, handler: Arc>, initialization_scripts: Arc>, + // Serialized origin of the main frame that initiated this request, captured + // browser-side in the scheme handler factory. The renderer can issue an IPC + // request before its execution context is fully wired to the loader; in + // that window Chromium tags the request with `Origin: null` even though the + // document already has a proper origin. We use this to repair the `Origin` + // header in that case. `None` when the initiator is not the (non-opaque) + // main frame, so sandboxed/subframe `Origin: null` requests are left as-is. + initiator_origin: Option, // we clone response to send it to the handler thread response: HttpResponse, } @@ -201,40 +251,32 @@ wrap_resource_handler! { let response_store = ThreadSafe(self.response.clone()); let initialization_scripts = self.initialization_scripts.clone(); let responder = Box::new(move |response: http::Response>| { - // Check if this is an HTML response that needs script injection - let content_type = response.headers().get(CONTENT_TYPE); - let is_html = content_type + let is_html = response + .headers() + .get(CONTENT_TYPE) .and_then(|ct| ct.to_str().ok()) .map(|ct| ct.to_lowercase().starts_with("text/html")) .unwrap_or(false); let (parts, body) = response.into_parts(); let body_bytes = body.into_owned(); - - let modified_body = if is_html { - inject_scripts_into_html_body(&body_bytes, &initialization_scripts) - .unwrap_or(body_bytes) + let body_bytes = if is_html { + inject_scripts_into_html_body(&body_bytes, &initialization_scripts).unwrap_or(body_bytes) } else { body_bytes }; - let mut response = http::Response::from_parts(parts, Cursor::new(modified_body)); - - - let csp = response - .headers_mut() - .get_mut(CONTENT_SECURITY_POLICY); + let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes)); - if let Some(csp) = csp { - let csp_string = csp.to_str().unwrap().to_string(); - let new_csp = csp_inject_initialization_scripts_hashes( - csp_string, - &initialization_scripts, - ); - *csp = HeaderValue::from_str(&new_csp).unwrap(); + if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) { + let csp_string = csp.to_str().unwrap_or_default().to_string(); + let new_csp = + csp_inject_initialization_scripts_hashes(csp_string, &initialization_scripts); + if let Ok(new_csp) = HeaderValue::from_str(&new_csp) { + *csp = new_csp; + } } - response_store.into_owned().borrow_mut().replace(response); let callback = callback.into_owned(); @@ -245,7 +287,27 @@ wrap_resource_handler! { let handler = self.handler.clone(); let data = read_request_body(request); - let headers = get_request_headers(request); + let mut headers = get_request_headers(request); + + // The renderer can issue an IPC request before its execution context is + // fully wired to the loader; in that window Chromium sends the request + // with `Origin: null` even though the document already has a real + // origin. Repair it from the initiating main frame's URL, which the + // browser process tracks reliably. Only done when the renderer sent no + // origin or a literal `null`, so a correct renderer-sent origin always + // wins. + if let Some(initiator_origin) = &self.initiator_origin { + let origin_missing_or_null = headers + .get(ORIGIN) + .map(|value| value.as_bytes() == b"null") + .unwrap_or(true); + if origin_missing_or_null + && let Ok(value) = HeaderValue::from_str(initiator_origin) + { + headers.insert(ORIGIN, value); + } + } + let method_str = CefString::from(&request.method()).to_string(); let method = http::Method::from_bytes(method_str.as_bytes()) .unwrap_or(http::Method::GET); @@ -297,7 +359,7 @@ wrap_resource_handler! { response.set_status(response_data.status().as_u16() as i32); let mut content_type = None; - // First pass: collect CSP header and set other headers + // Set response headers and remember the MIME type for CEF. for (name, value) in response_data.headers() { let Ok(value) = value.to_str() else { continue; }; @@ -340,7 +402,7 @@ wrap_scheme_handler_factory! { fn create( &self, browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, + frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, _request: Option<&mut Request>, ) -> Option { @@ -355,7 +417,24 @@ wrap_scheme_handler_factory! { .get(&(id, self.scheme.clone())) .cloned()?; - Some(WebResourceHandler::new(webview_label, handler, initialization_scripts, Arc::new(RefCell::new(None)))) + // Capture the initiating main frame's origin so `process_request` can + // repair a racy `Origin: null` header. Restricted to the main frame: it + // is never an opaque-origin (sandboxed) document in a Tauri webview, so + // upgrading its origin is safe; subframes are intentionally left alone. + let initiator_origin = frame + .filter(|frame| frame.is_main() == 1) + .map(|frame| CefString::from(&frame.url()).to_string()) + .and_then(|url| Url::parse(&url).ok()) + .map(|url| url.origin().ascii_serialization()) + .filter(|origin| origin != "null"); + + Some(WebResourceHandler::new( + webview_label, + handler, + initialization_scripts, + initiator_origin, + Arc::new(RefCell::new(None)), + )) } } } diff --git a/crates/tauri-runtime-cef/src/cef_webview.rs b/crates/tauri-runtime-cef/src/cef_webview.rs index f82d922220ee..65d6c7c9caf8 100644 --- a/crates/tauri-runtime-cef/src/cef_webview.rs +++ b/crates/tauri-runtime-cef/src/cef_webview.rs @@ -27,6 +27,7 @@ impl CefWebview { } } + #[allow(dead_code)] pub fn browser_id(&self) -> i32 { match self { CefWebview::BrowserView(view) => view.browser().map_or(-1, |b| b.identifier()), @@ -80,7 +81,16 @@ impl CefWebview { pub fn close(&self) { match self { - CefWebview::BrowserView(_) => {} + CefWebview::BrowserView(view) => { + // Alloy `BrowserView` (used for child webviews after the first): no + // platform window to destroy, so we must drive CEF's normal close + // lifecycle directly. Without this, dropping the `BrowserView` + // refptr leaves the browser alive in CEF's internal state, which + // hangs `cef::shutdown` and prevents the main process from exiting. + if let Some(host) = view.browser().and_then(|b| b.host()) { + let _ = host.try_close_browser(); + } + } CefWebview::Browser(browser) => browser.close(), } } diff --git a/crates/tauri-runtime-cef/src/cef_webview/macos.rs b/crates/tauri-runtime-cef/src/cef_webview/macos.rs index 5323f52d26e1..427bd4dba2ed 100644 --- a/crates/tauri-runtime-cef/src/cef_webview/macos.rs +++ b/crates/tauri-runtime-cef/src/cef_webview/macos.rs @@ -75,11 +75,17 @@ impl CefBrowserExt for cef::Browser { } fn close(&self) { - let Some(nsview) = self.nsview() else { + // Equivalent of Linux's `XDestroyWindow` / Windows's `DestroyWindow`: + // drive CEF's normal close lifecycle (DoClose → OnBeforeClose) so the + // browser and its renderer process are fully torn down. Detaching the + // NSView via `removeFromSuperview` (the previous behavior) does not + // trigger CEF's close on macOS, so the CEF browser remained alive after + // `webview.close()` — that leaked the renderer helper and hung + // `cef::shutdown` (and therefore the main process) on app exit. + let Some(host) = self.host() else { return; }; - - unsafe { nsview.removeFromSuperview() }; + let _ = host.try_close_browser(); } fn set_parent(&self, parent: &cef::Window) { diff --git a/crates/tauri-runtime-cef/src/cef_webview/windows.rs b/crates/tauri-runtime-cef/src/cef_webview/windows.rs index c4af7caf857b..40ede5b67445 100644 --- a/crates/tauri-runtime-cef/src/cef_webview/windows.rs +++ b/crates/tauri-runtime-cef/src/cef_webview/windows.rs @@ -103,21 +103,26 @@ impl CefBrowserExt for cef::Browser { } } -/// Toggle visibility on Chrome_WidgetWin_1 children that may have lost their -/// Chrome_RenderWidgetHostHWND render target (probably destroyed by CDP freeze). +/// Toggle visibility on Chrome_WidgetWin_1 children to force CEF to rebind +/// the compositor surface and emit a fresh paint after the child webview has +/// been hidden. +/// +/// Required when the child has been hidden, particularly when paired with CDP +/// `Page.setWebLifecycleState: frozen`, which pauses the renderer's compositor. +/// The Chrome_RenderWidgetHostHWND host window survives the freeze, but its +/// compositor surface is stale and won't repaint on its own when the parent is +/// shown again — leaving the widget visible with blank content. The hide+show +/// dance on Chrome_WidgetWin_1 forces Chromium to rebind the surface and +/// schedule a paint. unsafe fn ensure_render_target(hwnd: HWND) { use windows::core::PCWSTR; const CHROME_WIDGET: PCWSTR = windows::core::w!("Chrome_WidgetWin_1"); - const RENDER_TARGET: PCWSTR = windows::core::w!("Chrome_RenderWidgetHostHWND"); let mut child = unsafe { FindWindowExW(Some(hwnd), None, CHROME_WIDGET, PCWSTR::null()) }; while let Ok(child_hwnd) = child { - if unsafe { FindWindowExW(Some(child_hwnd), None, RENDER_TARGET, PCWSTR::null()).is_err() } { - // Hide and show the child to force CEF to recreate the render target - let _ = unsafe { ShowWindow(child_hwnd, SW_HIDE) }; - let _ = unsafe { ShowWindow(child_hwnd, SW_SHOW) }; - } + let _ = unsafe { ShowWindow(child_hwnd, SW_HIDE) }; + let _ = unsafe { ShowWindow(child_hwnd, SW_SHOW) }; child = unsafe { FindWindowExW(Some(hwnd), Some(child_hwnd), CHROME_WIDGET, PCWSTR::null()) }; } } diff --git a/crates/tauri-runtime-cef/src/lib.rs b/crates/tauri-runtime-cef/src/lib.rs index 64ff59058b12..f1dfbf94aa35 100644 --- a/crates/tauri-runtime-cef/src/lib.rs +++ b/crates/tauri-runtime-cef/src/lib.rs @@ -35,6 +35,7 @@ use std::{ collections::HashMap, fmt, fs::create_dir_all, + path::PathBuf, sync::{ Arc, Mutex, atomic::AtomicBool, @@ -217,6 +218,7 @@ enum WindowMessage { pub enum WebviewMessage { AddEventListener(WebviewEventId, Box), EvaluateScript(String), + EvaluateScriptWithCallback(String, Box), CookiesForUrl(Url, Sender>>>), Cookies(Sender>>>), SetCookie(Cookie<'static>), @@ -359,20 +361,20 @@ unsafe impl Sync for RuntimeContext {} impl RuntimeContext { fn post_message(&self, message: Message) -> Result<()> { - if thread::current().id() == self.main_thread_id { - // Already on main thread, execute directly + if thread::current().id() == self.main_thread_id && !cef_impl::is_in_event_callback() { + // On main thread and not inside a user callback, execute directly. cef_impl::handle_message(&self.cef_context, message); - Ok(()) } else { - // Post to main thread via TaskRunner + // Off main thread, or inside a user callback where synchronous execution + // could cause re-entrancy and deadlocks. Defer through the CEF TaskRunner. self .main_thread_task_runner .post_task(Some(&mut cef_impl::SendMessageTask::new( self.cef_context.clone(), Arc::new(RefCell::new(message)), ))); - Ok(()) } + Ok(()) } fn create_window( @@ -1186,6 +1188,18 @@ impl WebviewDispatch for CefWebviewDispatcher { }) } + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self.context.post_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), + }) + } + fn url(&self) -> Result { webview_getter!(self, WebviewMessage::Url)? } @@ -1954,9 +1968,10 @@ impl CefRuntime { let windows: Arc>> = Default::default(); #[cfg(target_os = "macos")] - let (_sandbox, _loader) = { - let is_helper = is_cef_helper_process(); + let is_helper = is_cef_helper_process(); + #[cfg(target_os = "macos")] + let (_sandbox, _loader) = { #[cfg(feature = "sandbox")] let sandbox = if is_helper { let mut sandbox = cef::sandbox::Sandbox::new(); @@ -1972,37 +1987,26 @@ impl CefRuntime { cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), is_helper); assert!(loader.load()); - if !is_helper { - let event_tx_ = event_tx.clone(); - let windows_ = windows.clone(); - init_ns_app(Box::new(move |event| match event { - AppDelegateEvent::ShouldTerminate { tx } => { - // Cancel macOS termination — we handle shutdown ourselves. - // - // Start closing all browsers (including devtools). The actual - // destruction is async and completes via the CEF message loop. - // - // Signal the main loop to exit. The post-loop safety net will - // pump the message loop until all browsers are fully destroyed - // before calling cef::shutdown(). - - tx.send(objc2_app_kit::NSApplicationTerminateReply::TerminateCancel) - .unwrap(); - event_tx_.send(RunEvent::Exit).unwrap(); - } - AppDelegateEvent::OpenURLs { urls } => { - event_tx_.send(RunEvent::Opened { urls }).unwrap(); - } - })); - } - (sandbox, loader) }; let _ = cef::api_hash(cef::sys::CEF_API_VERSION_LAST, 0); - let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); - let cache_path = cache_base.join(&runtime_args.identifier).join("cef"); + let mut command_line_args = Vec::new(); + let mut deep_link_schemes = Vec::new(); + let mut cache_path_override = None::; + for arg in runtime_args.platform_specific_attributes { + match arg { + RuntimeInitAttribute::CommandLineArgs { args } => command_line_args.extend(args), + RuntimeInitAttribute::DeepLinkSchemes { schemes } => deep_link_schemes.extend(schemes), + RuntimeInitAttribute::CachePath { path } => cache_path_override = Some(path), + } + } + + let cache_path = cache_path_override.unwrap_or_else(|| { + let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); + cache_base.join(&runtime_args.identifier).join("cef") + }); // Ensure the cache directory exists let _ = create_dir_all(&cache_path); @@ -2018,16 +2022,18 @@ impl CefRuntime { next_window_id: Default::default(), next_window_event_id: Default::default(), scheme_handler_registry: Default::default(), + cache_path: Arc::new(cache_path.clone()), + is_shutting_down: Default::default(), }; - let mut command_line_args = Vec::new(); - let mut deep_link_schemes = Vec::new(); - for arg in runtime_args.platform_specific_attributes { - match arg { - RuntimeInitAttribute::CommandLineArgs { args } => command_line_args.extend(args), - RuntimeInitAttribute::DeepLinkSchemes { schemes } => deep_link_schemes.extend(schemes), - } + // Promote `NSApp` to our `SimpleApplication` subclass *before* CEF (or + // anything else) touches `NSApp`, so it carries the `CefAppProtocol` + // conformance CEF requires. The delegate is installed later — see below. + #[cfg(target_os = "macos")] + if !is_helper { + init_ns_app(); } + command_line_args.push(("--enable-media-stream".to_string(), None)); let mut app = cef_impl::TauriApp::new( @@ -2070,6 +2076,45 @@ impl CefRuntime { 1 ); + // Install our `NSApplication` delegate *after* `cef::initialize`: CEF's + // Chrome runtime installs its own `AppController` as `NSApp.delegate` + // during initialization, so a delegate set earlier would be overwritten. + // cefsimple does the same — it assigns `NSApp.delegate` only once + // `CefInitialize` has returned. `SimpleApplication` also overrides + // `-terminate:` so AppKit can never `exit()` out from under us; Chromium + // must leave the run loop to shut down cleanly. + #[cfg(target_os = "macos")] + if !is_helper { + let event_tx_ = event_tx.clone(); + let cef_context_ = cef_context.clone(); + init_ns_app_delegate(Box::new(move |event| match event { + AppDelegateEvent::TryTerminate => { + // cefsimple/cefclient's `tryToTerminateApplication:` -> + // `CloseAllBrowsers(false)`: hand the request off to the runtime + // so the embedder sees `ExitRequested` (and may veto). If + // approved, the runtime sets `is_shutting_down` and the post-loop + // tear-down pumps the CEF message loop until every browser/window + // has gone through `OnBeforeClose`, then calls `cef::shutdown`. + cef_impl::handle_message(&cef_context_, Message::RequestExit(0)); + } + AppDelegateEvent::Reopen { + has_visible_windows, + } => { + event_tx_ + .send(RunEvent::Reopen { + has_visible_windows, + }) + .unwrap(); + } + AppDelegateEvent::AccessibilityChanged { enabled } => { + cef_impl::set_browsers_accessibility_state(&cef_context_, enabled); + } + AppDelegateEvent::OpenURLs { urls } => { + event_tx_.send(RunEvent::Opened { urls }).unwrap(); + } + })); + } + let main_thread_id = thread::current().id(); let context = RuntimeContext { main_thread_task_runner: cef::task_runner_get_for_current_thread().expect("null task runner"), @@ -2103,9 +2148,11 @@ pub fn run_cef_helper_process() { loader }; + let _ = cef::api_hash(cef::sys::CEF_API_VERSION_LAST, 0); + let mut app = cef_impl::TauriRenderApp::new(); cef::execute_process( Some(args.as_main_args()), - None::<&mut cef::App>, + Some(&mut app), std::ptr::null_mut(), ); } @@ -2116,6 +2163,10 @@ pub enum RuntimeInitAttribute { CommandLineArgs { args: Vec<(String, Option)> }, /// Deep link schemes. DeepLinkSchemes { schemes: Vec }, + /// Directory used for CEF disk cache (`Settings::cache_path`). + /// + /// If unspecified, defaults to `{user cache}/{app identifier}/cef`. + CachePath { path: PathBuf }, } impl InitAttribute for RuntimeInitAttribute { @@ -2150,6 +2201,10 @@ impl InitAttribute for RuntimeInitAttribute { /// Webview attributes. pub enum WebviewAtribute { /// Sets the browser runtime style. + /// + /// External file drag and drop events require [`RuntimeStyle::Alloy`]. + /// CEF's Chrome runtime does not currently route those events through + /// `CefDragHandler`, so Tauri drag/drop events will not be emitted there. RuntimeStyle { style: RuntimeStyle }, } @@ -2159,11 +2214,16 @@ pub enum RuntimeStyle { /// Alloy runtime. /// /// Used by default on multiwebview mode. + /// + /// Required for Tauri drag/drop events because CEF routes external file + /// drags through `CefDragHandler` only for Alloy-style webviews. Alloy, /// Chrome runtime. /// /// Used by default on webview window mode. /// + /// Does not currently support Tauri drag/drop events for external files. + /// /// Only a single browser view can use the Chrome runtime in a given window. Chrome, } @@ -2375,7 +2435,26 @@ impl Runtime for CefRuntime { (self.context.cef_context.callback.borrow())(RunEvent::MainEventsCleared); } - // We need to run the message loop until all windows are closed. Otherwise, we run into use after free crashes. + // Tear-down phase — mirrors the tail of cefclient's `RunMain`: + // `message_loop->Run()` returns, then `context->Shutdown()` runs and + // objects are released. cefclient is able to call `Shutdown()` directly + // because `RootWindowManager::CleanupOnUIThread` had already waited for + // every `OnBeforeClose`; our embedder-driven main loop breaks earlier on + // the `Exit` signal, so we have to drive the equivalent wait ourselves: + // close any still-open windows cooperatively (CEF's normal + // `can_close -> close_window_browsers -> OnBeforeClose -> on_window_destroyed` + // chain) and pump until the windows map is empty before calling + // `cef::shutdown`. Skipping this step trips use-after-free in CEF. + // + // Mark shut-down state defensively in case we got here via a path that + // didn't set it (e.g. an embedder-emitted Exit). With it set, the + // per-window callbacks during the drain stay silent. + self + .context + .cef_context + .is_shutting_down + .store(true, std::sync::atomic::Ordering::SeqCst); + cef_impl::close_all_windows(&self.context.cef_context.windows); while !self.context.cef_context.windows.borrow().is_empty() { cef::do_message_loop_work(); @@ -2383,8 +2462,11 @@ impl Runtime for CefRuntime { cef::shutdown(); - // Final Exit event - // use callback_ directly because cef_context.callback posts Exit events to the event loop rx + // Deliver the terminal `Exit` to the embedder. The wrapper above routes + // every `Exit` to the channel (so the main loop can break) and never + // forwards it to the user callback, so this final call is what the + // embedder actually observes — matching cefclient where the process + // returns from `main()` once shutdown completes. (callback_.borrow_mut())(RunEvent::Exit); } @@ -2393,26 +2475,25 @@ impl Runtime for CefRuntime { } } +/// Promotes the process's `NSApplication` to our `SimpleApplication` subclass. +/// +/// Must run before CEF (or anything else) touches `NSApp`: the first call to +/// `+[NSApplication sharedApplication]` fixes the concrete class of `NSApp`, +/// and CEF requires it to be a `CefAppProtocol` conformer. The delegate is +/// installed separately, *after* `cef::initialize` — see [`init_ns_app_delegate`]. #[cfg(target_os = "macos")] -fn init_ns_app(on_event: Box) { +fn init_ns_app() { use objc2::{ClassType, MainThreadMarker, msg_send, rc::Retained, runtime::NSObjectProtocol}; use objc2_app_kit::{NSApp, NSApplication}; - use application::{AppDelegate, SimpleApplication}; + use application::SimpleApplication; let mtm = MainThreadMarker::new().unwrap(); - unsafe { - // Initialize the SimpleApplication instance. - // SAFETY: mtm ensures that here is the main thread. - - use objc2::runtime::ProtocolObject; - - let app: Retained = msg_send![SimpleApplication::class(), sharedApplication]; - let delegate = AppDelegate::new(mtm, on_event); - let proto_delegate = ProtocolObject::from_ref(&*delegate); - app.setDelegate(Some(proto_delegate)); - } + // Initialize the SimpleApplication instance. + // SAFETY: mtm ensures that here is the main thread. + let _: Retained = + unsafe { msg_send![SimpleApplication::class(), sharedApplication] }; // If there was an invocation to NSApp prior to here, // then the NSApp will not be a SimpleApplication. @@ -2420,26 +2501,65 @@ fn init_ns_app(on_event: Box) { assert!(NSApp(mtm).isKindOfClass(SimpleApplication::class())); } +/// Installs our `AppDelegate` as the `NSApplication` delegate. +/// +/// Must run *after* `cef::initialize`: CEF's Chrome runtime installs its own +/// `AppController` as `NSApp.delegate` during initialization, so a delegate +/// set earlier is silently overwritten. This mirrors cefsimple, which assigns +/// `NSApp.delegate` only once `CefInitialize` has returned. +#[cfg(target_os = "macos")] +fn init_ns_app_delegate(on_event: Box) { + use objc2::{MainThreadMarker, rc::Retained, runtime::ProtocolObject}; + use objc2_app_kit::{NSApp, NSApplication}; + + use application::AppDelegate; + + let mtm = MainThreadMarker::new().unwrap(); + + let delegate = AppDelegate::new(mtm, on_event); + + // `init_ns_app` has already promoted `NSApp` to `SimpleApplication`. + let app: Retained = NSApp(mtm); + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + + // `NSApp.delegate` is only a weak reference and AppKit/CEF may repoint it, + // so hand the delegate to `APP_DELEGATE` — that owns it and is where + // `-terminate:` / `-accessibilitySetValue:` read it back. + application::set_app_delegate(delegate); +} + #[cfg(target_os = "macos")] mod application { - use std::{cell::Cell, sync::mpsc::channel}; + use std::cell::{Cell, RefCell}; use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; use objc2::{ DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, rc::Retained, - runtime::{Bool, NSObject, NSObjectProtocol}, + runtime::{AnyObject, Bool, NSObject, NSObjectProtocol}, }; - use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSApplicationTerminateReply}; - use objc2_foundation::{NSArray, NSURL}; + use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSApplicationTerminateReply, NSEvent}; + use objc2_foundation::{NSArray, NSString, NSURL}; + /// Application-level events surfaced from the macOS `NSApplication` / + /// delegate plumbing to the CEF runtime. pub enum AppDelegateEvent { - ShouldTerminate { - tx: std::sync::mpsc::Sender, - }, - OpenURLs { - urls: Vec, - }, + /// macOS requested an orderly quit (Cmd+Q, dock "Quit", Apple-menu + /// "Quit", Activity Monitor "Quit", logout, restart, shutdown). + /// + /// Delivered via the overridden `-terminate:` → + /// `tryToTerminateApplication:` rather than `-applicationShouldTerminate:`, + /// matching cefsimple/cefclient. The default `-terminate:` calls `exit()` + /// and never returns, which is incompatible with Chromium's need to leave + /// the run loop for an orderly shutdown. + TryTerminate, + /// The dock icon was clicked while the app was already running. + Reopen { has_visible_windows: bool }, + /// Assistive technology (e.g. VoiceOver) was enabled or disabled, + /// detected via the undocumented `AXEnhancedUserInterface` attribute. + AccessibilityChanged { enabled: bool }, + /// The OS asked the app to open URLs (deep links / file associations). + OpenURLs { urls: Vec }, } pub struct CefAppDelegateIvars { @@ -2468,22 +2588,64 @@ mod application { }) .collect(); - let handler = &self.ivars().on_event; - handler(AppDelegateEvent::OpenURLs { + (self.ivars().on_event)(AppDelegateEvent::OpenURLs { urls: converted_urls, }); } + // Not the termination entry point: `SimpleApplication`'s `-terminate:` + // override routes through `tryToTerminateApplication:` instead (the + // cefsimple/cefclient pattern). Kept as a defensive default for any + // code path that reaches `-applicationShouldTerminate:` directly. #[unsafe(method(applicationShouldTerminate:))] unsafe fn applicationShouldTerminate( &self, _sender: &NSApplication, ) -> NSApplicationTerminateReply { - let (tx, rx) = channel(); - let handler = &self.ivars().on_event; - handler(AppDelegateEvent::ShouldTerminate { tx }); - rx.try_recv() - .unwrap_or(NSApplicationTerminateReply::TerminateNow) + NSApplicationTerminateReply::TerminateNow + } + + // Called when the user clicks the dock icon while the app is running. + // Returning `false` leaves window management to the embedder. + #[unsafe(method(applicationShouldHandleReopen:hasVisibleWindows:))] + unsafe fn applicationShouldHandleReopen_hasVisibleWindows( + &self, + _sender: &NSApplication, + has_visible_windows: bool, + ) -> bool { + (self.ivars().on_event)(AppDelegateEvent::Reopen { + has_visible_windows, + }); + false + } + + // Opt into secure state restoration (macOS 12+). Matches + // cefsimple/cefclient: silences the AppKit warning and avoids macOS + // incorrectly restoring windows after a hard reset (power-button hold). + #[unsafe(method(applicationSupportsSecureRestorableState:))] + unsafe fn applicationSupportsSecureRestorableState(&self, _app: &NSApplication) -> bool { + true + } + } + + // Selectors invoked directly by `SimpleApplication` (not part of any + // protocol). They are registered as ObjC methods so the application + // subclass can `msg_send!` them on its delegate. + #[allow(non_snake_case)] + impl AppDelegate { + // Declared to return `BOOL` to match the signature CEF's Chrome-runtime + // `AppController` exposes for this selector, keeping the ObjC method + // encoding consistent with the platform convention. + #[unsafe(method(tryToTerminateApplication:))] + fn tryToTerminateApplication(&self, _app: &NSApplication) { + (self.ivars().on_event)(AppDelegateEvent::TryTerminate); + } + + #[unsafe(method(enableAccessibility:))] + fn enableAccessibility(&self, enabled: Bool) { + (self.ivars().on_event)(AppDelegateEvent::AccessibilityChanged { + enabled: enabled.as_bool(), + }); } } ); @@ -2501,8 +2663,31 @@ mod application { handling_send_event: Cell, } + thread_local! { + /// The runtime's `CefAppDelegate`, owned for the lifetime of the process. + /// + /// `SimpleApplication`'s `-terminate:` and `-accessibilitySetValue:` + /// route here directly instead of through `NSApp.delegate`: CEF's Chrome + /// runtime can repoint `NSApp.delegate` at its own `AppController`, and + /// forwarding there would bypass the runtime's orderly-exit flow. This + /// also keeps the delegate alive — `NSApp.delegate` is only a weak + /// reference. Main-thread-only, like `NSApp` itself. + /// + /// It cannot live in a `SimpleApplication` ivar: `NSApp` is created by + /// `+[NSApplication sharedApplication]`, not through objc2, so non-trivial + /// ivars are never initialized and accessing them panics. + static APP_DELEGATE: RefCell>> = const { RefCell::new(None) }; + } + + /// Records the application delegate. Call once, from the main thread, after + /// `cef::initialize`. + pub(super) fn set_app_delegate(delegate: Retained) { + APP_DELEGATE.with_borrow_mut(|slot| *slot = Some(delegate)); + } + define_class!( - /// A `NSApplication` subclass that implements the required CEF protocols. + /// A `NSApplication` subclass that implements the required CEF protocols + /// and the AppKit hooks a CEF embedder needs on macOS. /// /// This class provides the necessary `CefAppProtocol` conformance to /// ensure that events are handled correctly by the Chromium framework on macOS. @@ -2525,5 +2710,63 @@ mod application { } unsafe impl CefAppProtocol for SimpleApplication {} + + #[allow(non_snake_case)] + impl SimpleApplication { + // CEF requires `isHandlingSendEvent` to be true while AppKit dispatches + // an event — the `CefScopedSendingEvent` RAII guard from + // cefsimple/cefclient. Save and restore the previous value so nested + // `-sendEvent:` calls (modal loops, event tracking) stay correct. + #[unsafe(method(sendEvent:))] + unsafe fn sendEvent(&self, event: &NSEvent) { + let was_handling = self.ivars().handling_send_event.get(); + self.ivars().handling_send_event.set(Bool::YES); + let _: () = unsafe { msg_send![super(self), sendEvent: event] }; + self.ivars().handling_send_event.set(was_handling); + } + + // `-terminate:` is the entry point for every orderly macOS quit. The + // default implementation calls `exit()` and never returns, which is + // incompatible with Chromium — it depends on leaving the run loop to + // shut down cleanly. Hand off to our delegate's + // `tryToTerminateApplication:` and return without exiting; the runtime + // exits on its own once shutdown completes. + #[unsafe(method(terminate:))] + unsafe fn terminate(&self, _sender: Option<&AnyObject>) { + // Route to *our* `CefAppDelegate` (`APP_DELEGATE`), not + // `NSApp.delegate`: CEF's Chrome runtime can swap the public delegate + // for its own `AppController`, and forwarding there would bypass the + // runtime's `TryTerminate` -> `RequestExit` orderly-exit flow. + let delegate = APP_DELEGATE.with_borrow(|slot| slot.clone()); + if let Some(delegate) = delegate { + let _: () = unsafe { msg_send![&*delegate, tryToTerminateApplication: self] }; + } + } + + // Detect VoiceOver dynamically the same way Chromium does: the + // undocumented `AXEnhancedUserInterface` accessibility attribute is set + // to 1 when VoiceOver starts and 0 when it stops. + #[unsafe(method(accessibilitySetValue:forAttribute:))] + unsafe fn accessibilitySetValue_forAttribute( + &self, + value: Option<&AnyObject>, + attribute: Option<&NSString>, + ) { + if let (Some(value), Some(attribute)) = (value, attribute) { + if attribute.to_string() == "AXEnhancedUserInterface" { + let int_value: std::os::raw::c_int = unsafe { msg_send![value, intValue] }; + // Route to our own delegate — see `terminate:` above for why + // `NSApp.delegate` cannot be trusted once CEF has initialized. + let delegate = APP_DELEGATE.with_borrow(|slot| slot.clone()); + if let Some(delegate) = delegate { + let _: () = + unsafe { msg_send![&*delegate, enableAccessibility: Bool::new(int_value == 1)] }; + } + } + } + + unsafe { msg_send![super(self), accessibilitySetValue: value, forAttribute: attribute] } + } + } ); } diff --git a/crates/tauri-runtime-wry/CHANGELOG.md b/crates/tauri-runtime-wry/CHANGELOG.md index 3d01fa27e84b..3f31a89f6380 100644 --- a/crates/tauri-runtime-wry/CHANGELOG.md +++ b/crates/tauri-runtime-wry/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-runtime@2.11.1` + +## \[2.11.0] + +### New Features + +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`b27be063f`](https://www.github.com/tauri-apps/tauri/commit/b27be063ff3052cb1071ac3ec719cfa104460fa4) ([#14925](https://www.github.com/tauri-apps/tauri/pull/14925)) Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Trigger `RunEvent::Opened` on Android. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). +- [`1063c48c5`](https://www.github.com/tauri-apps/tauri/commit/1063c48c5e7d099ad74d28a937edf42e3f5c9f03) ([#14523](https://www.github.com/tauri-apps/tauri/pull/14523)) Add handler for web content process termination on macOS and iOS. + +### Bug Fixes + +- [`110336c88`](https://www.github.com/tauri-apps/tauri/commit/110336c88a8c0a04476619db0a5c8f7694d969a5) ([#15250](https://www.github.com/tauri-apps/tauri/pull/15250)) Fix initial window position when positioning it to another monitor. +- [`9808236eb`](https://www.github.com/tauri-apps/tauri/commit/9808236ebf7755d498d674b614f3fc75eeac1ec4) ([#14655](https://www.github.com/tauri-apps/tauri/pull/14655)) Fix monitor work area Y position on macOS. + +### What's Changed + +- [`d34497ef1`](https://www.github.com/tauri-apps/tauri/commit/d34497ef154eddcc36327a30dda06dc4748f6b20) ([#14862](https://www.github.com/tauri-apps/tauri/pull/14862)) The new window handler passed to `on_new_window` no longer requires `Sync`, and runs on main thread on Windows, aligning with other platforms + +### Dependencies + +- Upgraded to `tauri-runtime@2.11.0` +- Upgraded to `tauri-utils@2.9.0` + ## \[2.10.1] ### Dependencies diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml index 7a46b4adf9e2..428cd1f25c16 100644 --- a/crates/tauri-runtime-wry/Cargo.toml +++ b/crates/tauri-runtime-wry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.1" description = "Wry bindings to the Tauri runtime" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -13,15 +13,14 @@ edition.workspace = true rust-version.workspace = true [dependencies] -wry = { version = "0.54.0", default-features = false, features = [ - "drag-drop", +wry = { version = "0.55.0", default-features = false, features = [ "protocol", "os-webview", "linux-body", ] } -tao = { version = "0.34.5", default-features = false, features = ["rwh_06"] } -tauri-runtime = { version = "2.10.1", path = "../tauri-runtime" } -tauri-utils = { version = "2.8.3", path = "../tauri-utils" } +tao = { version = "0.35.0", default-features = false, features = ["rwh_06"] } +tauri-runtime = { version = "2.11.1", path = "../tauri-runtime" } +tauri-utils = { version = "2.9.1", path = "../tauri-utils" } raw-window-handle = "0.6" http = "1" url = "2" @@ -61,7 +60,7 @@ objc2-web-kit = { version = "0.3", features = ["objc2-app-kit", "WKWebView"] } jni = "0.21" [features] -default = ["x11"] +default = ["x11", "dbus"] devtools = ["wry/devtools", "tauri-runtime/devtools"] x11 = ["tao/x11", "wry/x11"] macos-private-api = [ @@ -75,3 +74,4 @@ tracing = ["dep:tracing", "wry/tracing"] macos-proxy = ["wry/mac-proxy"] unstable = [] common-controls-v6 = [] +dbus = ["tao/dbus"] diff --git a/crates/tauri-runtime-wry/src/dialog/mod.rs b/crates/tauri-runtime-wry/src/dialog/mod.rs index 156d6a6d2a4c..3befa7b8a0bf 100644 --- a/crates/tauri-runtime-wry/src/dialog/mod.rs +++ b/crates/tauri-runtime-wry/src/dialog/mod.rs @@ -5,7 +5,9 @@ #[cfg(windows)] mod windows; -pub fn error>(err: S) { +// Takes a `&'static str` here since we convert clickable hyperlinks, +// DO NOT pass in untrusted input +pub fn error(err: &'static str) { #[cfg(windows)] windows::error(err); diff --git a/crates/tauri-runtime-wry/src/dialog/windows.rs b/crates/tauri-runtime-wry/src/dialog/windows.rs index faa8fab29a99..26850eb6e502 100644 --- a/crates/tauri-runtime-wry/src/dialog/windows.rs +++ b/crates/tauri-runtime-wry/src/dialog/windows.rs @@ -12,8 +12,8 @@ enum Level { Info, } -pub fn error>(err: S) { - dialog_inner(err.as_ref(), Level::Error); +pub fn error(err: &'static str) { + dialog_inner(err, Level::Error); } fn dialog_inner(err: &str, level: Level) { diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index 490741625366..38d0670693a6 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -35,6 +35,8 @@ use tauri_runtime::{ #[cfg(target_vendor = "apple")] use objc2::rc::Retained; +#[cfg(target_os = "android")] +use tao::platform::android::{WindowBuilderExtAndroid, WindowExtAndroid}; #[cfg(target_os = "macos")] use tao::platform::macos::{EventLoopWindowTargetExtMacOS, WindowBuilderExtMacOS}; #[cfg(any( @@ -78,12 +80,9 @@ use tao::{ UserAttentionType as TaoUserAttentionType, }, }; -#[cfg(target_os = "macos")] -use tauri_utils::TitleBarStyle; -#[cfg(desktop)] use tauri_utils::config::PreventOverflowConfig; use tauri_utils::{ - Theme, + Theme, TitleBarStyle, config::{Color, WindowConfig}, }; use url::Url; @@ -115,7 +114,7 @@ use wry::{ use wry::{WebViewBuilderExtUnix, WebViewExtUnix}; #[cfg(target_os = "ios")] -pub use tao::platform::ios::WindowExtIOS; +pub use tao::platform::ios::{WindowBuilderExtIOS, WindowExtIOS}; #[cfg(target_os = "macos")] pub use tao::platform::macos::{ ActivationPolicy as TaoActivationPolicy, EventLoopExtMacOS, WindowExtMacOS, @@ -756,6 +755,25 @@ impl From for PositionWrapper { } } +#[cfg(desktop)] +fn find_monitor_for_position( + monitors: impl Iterator, + window_position: TaoPosition, +) -> Option { + monitors.into_iter().find(|m| { + let monitor_pos = m.position(); + let monitor_size = m.size(); + + // type annotations required for 32bit targets. + let window_position = window_position.to_physical::(m.scale_factor()); + + monitor_pos.x <= window_position.x + && window_position.x < monitor_pos.x + monitor_size.width as i32 + && monitor_pos.y <= window_position.y + && window_position.y < monitor_pos.y + monitor_size.height as i32 + }) +} + #[derive(Debug, Clone)] pub struct UserAttentionTypeWrapper(pub TaoUserAttentionType); @@ -882,7 +900,7 @@ impl WindowBuilder for WindowBuilderWrapper { #[cfg(target_os = "macos")] { // TODO: find a proper way to prevent webview being pushed out of the window. - // Workround for issue: https://github.com/tauri-apps/tauri/issues/10225 + // Workaround for issue: https://github.com/tauri-apps/tauri/issues/10225 // The window requires `NSFullSizeContentViewWindowMask` flag to prevent devtools // pushing the content view out of the window. // By setting the default style to `TitleBarStyle::Visible` should fix the issue for most of the users. @@ -945,68 +963,91 @@ impl WindowBuilder for WindowBuilderWrapper { window.inner = window.inner.with_cursor_moved_event(false); } - #[cfg(desktop)] + #[cfg(target_os = "android")] { - window = window - .title(config.title.to_string()) - .inner_size(config.width, config.height) - .focused(config.focus) - .focusable(config.focusable) - .visible(config.visible) - .resizable(config.resizable) - .fullscreen(config.fullscreen) - .decorations(config.decorations) - .maximized(config.maximized) - .always_on_bottom(config.always_on_bottom) - .always_on_top(config.always_on_top) - .visible_on_all_workspaces(config.visible_on_all_workspaces) - .content_protected(config.content_protected) - .skip_taskbar(config.skip_taskbar) - .theme(config.theme) - .closable(config.closable) - .maximizable(config.maximizable) - .minimizable(config.minimizable) - .shadow(config.shadow); - - let mut constraints = WindowSizeConstraints::default(); - - if let Some(min_width) = config.min_width { - constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); - } - if let Some(min_height) = config.min_height { - constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); - } - if let Some(max_width) = config.max_width { - constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); + if let Some(activity_name) = &config.activity_name { + window.inner = window.inner.with_activity_name(activity_name.clone()); } - if let Some(max_height) = config.max_height { - constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); + if let Some(activity_name) = &config.created_by_activity_name { + window.inner = window + .inner + .with_created_by_activity_name(activity_name.clone()); } - if let Some(color) = config.background_color { - window = window.background_color(color); - } - window = window.inner_size_constraints(constraints); + } - if let (Some(x), Some(y)) = (config.x, config.y) { - window = window.position(x, y); + #[cfg(target_os = "ios")] + { + if let Some(scene_identifier) = &config.requested_by_scene_identifier { + window.inner = window + .inner + .with_requesting_scene_identifier(scene_identifier.clone()); } + } - if config.center { - window = window.center(); - } + // ignore size from config for mobile for backward compatibility + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + window = window.inner_size(config.width, config.height); + } - if let Some(window_classname) = &config.window_classname { - window = window.window_classname(window_classname); - } + window = window + .title(config.title.to_string()) + .focused(config.focus) + .focusable(config.focusable) + .visible(config.visible) + .resizable(config.resizable) + .fullscreen(config.fullscreen) + .decorations(config.decorations) + .maximized(config.maximized) + .always_on_bottom(config.always_on_bottom) + .always_on_top(config.always_on_top) + .visible_on_all_workspaces(config.visible_on_all_workspaces) + .content_protected(config.content_protected) + .skip_taskbar(config.skip_taskbar) + .theme(config.theme) + .closable(config.closable) + .maximizable(config.maximizable) + .minimizable(config.minimizable) + .shadow(config.shadow); + + let mut constraints = WindowSizeConstraints::default(); + + if let Some(min_width) = config.min_width { + constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); + } + if let Some(min_height) = config.min_height { + constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); + } + if let Some(max_width) = config.max_width { + constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); + } + if let Some(max_height) = config.max_height { + constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); + } + if let Some(color) = config.background_color { + window = window.background_color(color); + } + window = window.inner_size_constraints(constraints); - if let Some(prevent_overflow) = &config.prevent_overflow { - window = match prevent_overflow { - PreventOverflowConfig::Enable(true) => window.prevent_overflow(), - PreventOverflowConfig::Margin(margin) => window - .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()), - _ => window, - }; - } + if let (Some(x), Some(y)) = (config.x, config.y) { + window = window.position(x, y); + } + + if config.center { + window = window.center(); + } + + if let Some(window_classname) = &config.window_classname { + window = window.window_classname(window_classname); + } + + if let Some(prevent_overflow) = &config.prevent_overflow { + window = match prevent_overflow { + PreventOverflowConfig::Enable(true) => window.prevent_overflow(), + PreventOverflowConfig::Margin(margin) => window + .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()), + _ => window, + }; } window @@ -1322,6 +1363,26 @@ impl WindowBuilder for WindowBuilderWrapper { fn window_classname>(self, _window_classname: S) -> Self { self } + + #[cfg(target_os = "android")] + fn activity_name>(mut self, class_name: S) -> Self { + self.inner = self.inner.with_activity_name(class_name.into()); + self + } + + #[cfg(target_os = "android")] + fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.inner = self.inner.with_created_by_activity_name(class_name.into()); + self + } + + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(mut self, identifier: S) -> Self { + self.inner = self + .inner + .with_requesting_scene_identifier(identifier.into()); + self + } } #[cfg(any( @@ -1413,6 +1474,10 @@ pub enum WindowMessage { target_os = "openbsd" ))] GtkBox(Sender), + #[cfg(target_os = "android")] + ActivityName(Sender), + #[cfg(target_os = "ios")] + SceneIdentifier(Sender), RawWindowHandle(Sender>), Theme(Sender), IsEnabled(Sender), @@ -1461,7 +1526,7 @@ pub enum WindowMessage { SetBadgeLabel(Option), SetOverlayIcon(Option), SetProgressBar(ProgressBarState), - SetTitleBarStyle(tauri_utils::TitleBarStyle), + SetTitleBarStyle(TitleBarStyle), SetTrafficLightPosition(Position), SetTheme(Option), SetBackgroundColor(Option), @@ -1492,6 +1557,15 @@ pub enum WebviewMessage { EvaluateScript(String), #[cfg(all(feature = "tracing", not(target_os = "android")))] EvaluateScript(String, Sender<()>, tracing::Span), + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + EvaluateScriptWithCallback(String, Box), + #[cfg(all(feature = "tracing", not(target_os = "android")))] + EvaluateScriptWithCallback( + String, + Box, + Sender<()>, + tracing::Span, + ), CookiesForUrl(Url, Sender>>>), Cookies(Sender>>>), SetCookie(tauri_runtime::Cookie<'static>), @@ -1867,6 +1941,46 @@ impl WebviewDispatch for WryWebviewDispatcher { ) } + #[cfg(all(feature = "tracing", not(target_os = "android")))] + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + // use a channel so the EvaluateScript task uses the current span as parent + let (tx, rx) = channel(); + getter!( + self, + rx, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScriptWithCallback( + script.into(), + Box::new(callback), + tx, + tracing::Span::current(), + ), + ) + ) + } + + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), + ), + ) + } + fn set_zoom(&self, scale_factor: f64) -> Result<()> { send_user_message( &self.context, @@ -2094,6 +2208,18 @@ impl WindowDispatch for WryWindowDispatcher { window_getter!(self, WindowMessage::GtkBox).map(|w| w.0) } + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result { + window_getter!(self, WindowMessage::ActivityName) + } + + /// Returns the identifier of the UIScene tied to this UIWindow. + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result { + window_getter!(self, WindowMessage::SceneIdentifier) + } + fn window_handle( &self, ) -> std::result::Result, raw_window_handle::HandleError> { @@ -2465,7 +2591,7 @@ impl WindowDispatch for WryWindowDispatcher { ) } - fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + fn set_title_bar_style(&self, style: TitleBarStyle) -> Result<()> { send_user_message( &self.context, Message::Window(self.window_id, WindowMessage::SetTitleBarStyle(style)), @@ -3422,6 +3548,14 @@ fn handle_user_message( WindowMessage::GtkBox(tx) => tx .send(GtkBox(window.default_vbox().unwrap().clone())) .unwrap(), + #[cfg(target_os = "android")] + WindowMessage::ActivityName(tx) => { + tx.send(window.activity_name()).unwrap(); + } + #[cfg(target_os = "ios")] + WindowMessage::SceneIdentifier(tx) => { + tx.send(window.scene_identifier()).unwrap(); + } WindowMessage::RawWindowHandle(tx) => tx .send( window @@ -3749,6 +3883,20 @@ fn handle_user_message( log::error!("{e}"); } } + #[cfg(all(feature = "tracing", not(target_os = "android")))] + WebviewMessage::EvaluateScriptWithCallback(script, callback, tx, span) => { + let _span = span.entered(); + if let Err(e) = webview.evaluate_script_with_callback(&script, callback) { + log::error!("{e}"); + } + tx.send(()).unwrap(); + } + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + WebviewMessage::EvaluateScriptWithCallback(script, callback) => { + if let Err(e) = webview.evaluate_script_with_callback(&script, callback) { + log::error!("{e}"); + } + } WebviewMessage::Navigate(url) => { if let Err(e) = webview.load_url(url.as_str()) { log::error!("failed to navigate to url {}: {}", url, e); @@ -4037,13 +4185,9 @@ fn handle_user_message( } } Message::CreateWindow(window_id, handler) => match handler(event_loop) { - // wait for borrow_mut to be available - on Windows we might poll for the window to be inserted - Ok(webview) => loop { - if let Ok(mut windows) = windows.0.try_borrow_mut() { - windows.insert(window_id, webview); - break; - } - }, + Ok(webview) => { + windows.0.borrow_mut().insert(window_id, webview); + } Err(e) => { log::error!("{e}"); } @@ -4349,7 +4493,7 @@ fn handle_event_loop( ); } }, - #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] Event::Opened { urls } => { callback(RunEvent::Opened { urls }); } @@ -4360,6 +4504,35 @@ fn handle_event_loop( } => callback(RunEvent::Reopen { has_visible_windows, }), + #[cfg(target_os = "ios")] + Event::SceneRequested { scene, options } => { + callback(RunEvent::SceneRequested { scene, options }); + } + #[cfg(mobile)] + e @ Event::Resumed | e @ Event::Suspended => { + let event = match e { + Event::Resumed => WindowEvent::Resumed, + Event::Suspended => WindowEvent::Suspended, + _ => unreachable!(), + }; + + let windows_ref = windows.0.borrow(); + windows_ref.values().for_each(|window| { + let label = window.label.clone(); + let window_event_listeners = window.window_event_listeners.clone(); + let listeners = window_event_listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + + callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + }); + + drop(windows_ref); + } _ => (), } } @@ -4463,18 +4636,7 @@ fn create_window( #[cfg(desktop)] if window_builder.prevent_overflow.is_some() || window_builder.center { let monitor = if let Some(window_position) = &window_builder.inner.window.position { - event_loop.available_monitors().find(|m| { - let monitor_pos = m.position(); - let monitor_size = m.size(); - - // type annotations required for 32bit targets. - let window_position = window_position.to_physical::(m.scale_factor()); - - monitor_pos.x <= window_position.x - && window_position.x < monitor_pos.x + monitor_size.width as i32 - && monitor_pos.y <= window_position.y - && window_position.y < monitor_pos.y + monitor_size.height as i32 - }) + find_monitor_for_position(event_loop.available_monitors(), *window_position) } else { event_loop.primary_monitor() }; @@ -4541,11 +4703,35 @@ fn create_window( } }; + #[cfg(any(target_os = "macos", target_os = "linux"))] + let (initial_position, is_fullscreen) = ( + window_builder.inner.window.position, + window_builder.inner.window.fullscreen.is_some(), + ); + + // If fullscreen is requested with an explicit position, resolve the target + // monitor up front so the window is created fullscreen on that display. + #[cfg(any(target_os = "macos", target_os = "linux"))] + if let (true, Some(position)) = (is_fullscreen, initial_position) + && let Some(target_monitor) = + find_monitor_for_position(event_loop.available_monitors(), position) + { + window_builder.inner.window.fullscreen = Some(Fullscreen::Borderless(Some(target_monitor))); + } + let window = window_builder .inner .build(event_loop) + .inspect_err(|e| log::error!("Error creating window: {e:?}")) .map_err(|_| Error::CreateWindow)?; + // On macOS, `with_position` uses the content origin; the title bar is added + // above it. `set_outer_position` is needed for precise window placement. + #[cfg(target_os = "macos")] + if !is_fullscreen && let Some(position) = initial_position { + window.set_outer_position(position); + } + #[cfg(feature = "tracing")] { drop(window_create_span); @@ -4707,6 +4893,16 @@ You may have it installed on another user account, but it is not available for t let is_first_context = web_context.is_empty(); // the context must be stored on the HashMap because it must outlive the WebView on macOS let automation_enabled = std::env::var("TAURI_WEBVIEW_AUTOMATION").as_deref() == Ok("true"); + // Make sure the data directory exists before handing it to the web context, + // or WebView2 / WebKitGTK can panic while initializing the user data folder. + if let Some(user_data_dir) = webview_attributes + .data_directory + .as_ref() + .filter(|dir| !dir.exists()) + { + std::fs::create_dir_all(user_data_dir).map_err(|e| Error::CreateWebview(Box::new(e)))?; + } + let web_context_key = webview_attributes.data_directory; let entry = web_context.entry(web_context_key.clone()); let web_context = match entry { @@ -4737,7 +4933,8 @@ You may have it installed on another user account, but it is not available for t .with_accept_first_mouse(webview_attributes.accept_first_mouse) .with_incognito(webview_attributes.incognito) .with_clipboard(webview_attributes.clipboard) - .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled); + .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled) + .with_general_autofill_enabled(webview_attributes.general_autofill_enabled); if url != "about:blank" { webview_builder = webview_builder.with_url(&url); @@ -4841,63 +5038,56 @@ You may have it installed on another user account, but it is not available for t #[cfg(desktop)] let context = context.clone(); webview_builder = webview_builder.with_new_window_req_handler(move |url, features| { - url - .parse() - .map(|url| { - let response = new_window_handler( - url, - tauri_runtime::webview::NewWindowFeatures::new( - features.size, - features.position, - NewWindowOpener { - #[cfg(desktop)] - webview: features.opener.webview, - #[cfg(windows)] - environment: features.opener.environment, - #[cfg(target_os = "macos")] - target_configuration: features.opener.target_configuration, - }, - ), - ); - match response { - tauri_runtime::webview::NewWindowResponse::Allow => wry::NewWindowResponse::Allow, + let Ok(url) = url.parse() else { + return wry::NewWindowResponse::Deny; + }; + let response = new_window_handler( + url, + tauri_runtime::webview::NewWindowFeatures::new( + features.size, + features.position, + NewWindowOpener { #[cfg(desktop)] - tauri_runtime::webview::NewWindowResponse::Create { window_id } => { - let windows = &context.main_thread.windows.0; - let webview = loop { - if let Some(webview) = windows.try_borrow().ok().and_then(|windows| { - windows - .get(&window_id) - .map(|window| window.webviews.first().unwrap().clone()) - }) { - break webview; - } else { - // on Windows the window is created async so we should wait for it to be available - std::thread::sleep(std::time::Duration::from_millis(50)); - continue; - }; - }; - - #[cfg(desktop)] - wry::NewWindowResponse::Create { - #[cfg(target_os = "macos")] - webview: wry::WebViewExtMacOS::webview(&*webview).as_super().into(), - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - ))] - webview: webview.webview(), - #[cfg(windows)] - webview: webview.webview(), - } - } - tauri_runtime::webview::NewWindowResponse::Deny => wry::NewWindowResponse::Deny, + webview: features.opener.webview, + #[cfg(windows)] + environment: features.opener.environment, + #[cfg(target_os = "macos")] + target_configuration: features.opener.target_configuration, + }, + ), + ); + match response { + tauri_runtime::webview::NewWindowResponse::Allow => wry::NewWindowResponse::Allow, + #[cfg(desktop)] + tauri_runtime::webview::NewWindowResponse::Create { window_id } => { + let windows = &context.main_thread.windows.0; + let webview = windows + .borrow() + .get(&window_id) + .unwrap() + .webviews + .first() + .unwrap() + .clone(); + + #[cfg(desktop)] + wry::NewWindowResponse::Create { + #[cfg(target_os = "macos")] + webview: wry::WebViewExtMacOS::webview(&*webview).as_super().into(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + webview: webview.webview(), + #[cfg(windows)] + webview: webview.webview(), } - }) - .unwrap_or(wry::NewWindowResponse::Deny) + } + tauri_runtime::webview::NewWindowResponse::Deny => wry::NewWindowResponse::Deny, + } }); } @@ -5070,6 +5260,35 @@ You may have it installed on another user account, but it is not available for t webview_builder = webview_builder.with_allow_link_preview(webview_attributes.allow_link_preview); + + if let Some(on_web_content_process_terminate_handler) = + pending.on_web_content_process_terminate_handler + { + webview_builder = webview_builder + .with_on_web_content_process_terminate_handler(on_web_content_process_terminate_handler); + } else { + log::debug!("web content process terminated"); + let context_ = context.clone(); + let window_id_ = window_id.clone(); + webview_builder = webview_builder.with_on_web_content_process_terminate_handler(move || { + if let Ok(windows) = &context_.main_thread.windows.0.try_borrow() { + if let Some(window) = windows.get(&*window_id_.lock().unwrap()) { + if let Some(webview) = window.webviews.iter().find(|w| w.id == id) { + match webview.reload() { + Ok(_) => log::debug!("webview reloaded"), + Err(e) => log::error!("failed to reload webview: {}", e), + } + } else { + log::error!("failed to find webview") + } + } else { + log::error!("failed to get window") + } + } else { + log::error!("failed to borrow windows") + } + }); + } } #[cfg(target_os = "ios")] diff --git a/crates/tauri-runtime-wry/src/monitor/macos.rs b/crates/tauri-runtime-wry/src/monitor/macos.rs index 6ed9f3c1dd56..aca8e334d0a5 100644 --- a/crates/tauri-runtime-wry/src/monitor/macos.rs +++ b/crates/tauri-runtime-wry/src/monitor/macos.rs @@ -19,6 +19,8 @@ impl super::MonitorExt for tao::monitor::MonitorHandle { position.x += visible_frame.origin.x - screen_frame.origin.x; + position.y += (screen_frame.origin.y + screen_frame.size.height) + - (visible_frame.origin.y + visible_frame.size.height); PhysicalRect { size: LogicalSize::new(visible_frame.size.width, visible_frame.size.height) .to_physical(scale_factor), diff --git a/crates/tauri-runtime/CHANGELOG.md b/crates/tauri-runtime/CHANGELOG.md index 47263c499fe9..bbcb4bd54e71 100644 --- a/crates/tauri-runtime/CHANGELOG.md +++ b/crates/tauri-runtime/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.11.0] + +### New Features + +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`b27be063f`](https://www.github.com/tauri-apps/tauri/commit/b27be063ff3052cb1071ac3ec719cfa104460fa4) ([#14925](https://www.github.com/tauri-apps/tauri/pull/14925)) Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Trigger `RunEvent::Opened` on Android. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). +- [`1063c48c5`](https://www.github.com/tauri-apps/tauri/commit/1063c48c5e7d099ad74d28a937edf42e3f5c9f03) ([#14523](https://www.github.com/tauri-apps/tauri/pull/14523)) Add handler for web content process termination on macOS and iOS. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` + ## \[2.10.1] ### Dependencies diff --git a/crates/tauri-runtime/Cargo.toml b/crates/tauri-runtime/Cargo.toml index 1615c9768f74..f22ebac78add 100644 --- a/crates/tauri-runtime/Cargo.toml +++ b/crates/tauri-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.1" description = "Runtime for Tauri applications" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -27,7 +27,7 @@ targets = [ serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" -tauri-utils = { version = "2.8.3", path = "../tauri-utils" } +tauri-utils = { version = "2.9.1", path = "../tauri-utils" } http = "1" raw-window-handle = "0.6" url = { version = "2" } @@ -50,6 +50,8 @@ objc2 = "0.6" objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ "UIView", "UIResponder", + "UIScene", + "UISceneOptions", ] } [target."cfg(target_os = \"macos\")".dependencies] diff --git a/crates/tauri-runtime/src/lib.rs b/crates/tauri-runtime/src/lib.rs index 92feed0594fd..2780cf0fa4e4 100644 --- a/crates/tauri-runtime/src/lib.rs +++ b/crates/tauri-runtime/src/lib.rs @@ -242,6 +242,20 @@ pub enum RunEvent { }, /// A custom event defined by the user. UserEvent(T), + /// Emitted when a scene is requested by the system. + /// + /// This event is emitted when a scene is requested by the system. + /// Scenes created by [`Window::new`] are not emitted with this event. + /// It is also not emitted for the main scene. + #[cfg(target_os = "ios")] + SceneRequested { + /// Scene that was requested by the system. + scene: objc2::rc::Retained, + /// Options that were used to request the scene. + /// + /// This lets you determine why the scene was requested. + options: objc2::rc::Retained, + }, } /// Action to take when the event loop is about to exit @@ -621,6 +635,16 @@ pub trait WebviewDispatch: Debug + Clone + Send + Sync + Sized + ' /// Executes javascript on the window this [`WindowDispatch`] represents. fn eval_script>(&self, script: S) -> Result<()>; + /// Evaluate JavaScript with callback function on the webview this [`WebviewDispatch`] represents. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()>; + /// Moves the webview to the given window. fn reparent(&self, window_id: WindowId) -> Result<()>; @@ -792,6 +816,14 @@ pub trait WindowDispatch: Debug + Clone + Send + Sync + Sized + 's ))] fn default_vbox(&self) -> Result; + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result; + + /// Returns the identifier of the UIScene tied to this UIWindow. + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result; + /// Raw window handle. fn window_handle( &self, diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs index e8ccf44b1b2a..c648bb22c7b7 100644 --- a/crates/tauri-runtime/src/webview.rs +++ b/crates/tauri-runtime/src/webview.rs @@ -44,6 +44,9 @@ pub type AddressChangedHandler = dyn Fn(&Url) + Send + Sync + 'static; pub type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync; +#[cfg(any(target_os = "macos", target_os = "ios"))] +type OnWebContentProcessTerminateHandler = dyn Fn() + Send; + #[cfg(target_os = "ios")] type InputAccessoryViewBuilderFn = dyn Fn(&objc2_ui_kit::UIView) -> Option> + Send @@ -161,7 +164,7 @@ pub enum ScrollBarStyle { /// Fluent UI style overlay scrollbars. **Windows Only** /// /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + /// see FluentOverlay, } @@ -200,13 +203,16 @@ pub struct PendingWebview> { #[cfg(target_os = "android")] #[allow(clippy::type_complexity)] pub on_webview_created: - Option) -> Result<(), jni::errors::Error> + Send>>, + Option) -> Result<(), jni::errors::Error> + Send + Sync>>, pub web_resource_request_handler: Option>, pub on_page_load_handler: Option>, pub download_handler: Option>, + + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub on_web_content_process_terminate_handler: Option>, } impl> PendingWebview { @@ -237,6 +243,8 @@ impl> PendingWebview { web_resource_request_handler: None, on_page_load_handler: None, download_handler: None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate_handler: None, }) } } @@ -260,7 +268,7 @@ impl> PendingWebview { #[cfg(target_os = "android")] pub fn on_webview_created< - F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + 'static, + F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync + 'static, >( mut self, f: F, @@ -349,6 +357,24 @@ pub struct WebviewAttributes { /// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview pub allow_link_preview: bool, pub scroll_bar_style: ScrollBarStyle, + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// If not specified, this is `true` by default. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + pub general_autofill_enabled: bool, /// Allows overriding the keyboard accessory view on iOS. /// Returning `None` effectively removes the view. /// @@ -400,7 +426,8 @@ impl From<&WindowConfig> for WebviewAttributes { #[cfg(windows)] ConfigScrollBarStyle::FluentOverlay => ScrollBarStyle::FluentOverlay, _ => ScrollBarStyle::Default, - }); + }) + .general_autofill_enabled(config.general_autofill_enabled); #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] { @@ -475,6 +502,7 @@ impl WebviewAttributes { javascript_disabled: false, allow_link_preview: true, scroll_bar_style: ScrollBarStyle::Default, + general_autofill_enabled: true, #[cfg(target_os = "ios")] input_accessory_view_builder: None, } @@ -729,7 +757,7 @@ impl WebviewAttributes { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[must_use] pub fn background_throttling(mut self, policy: Option) -> Self { self.background_throttling = policy; @@ -754,6 +782,29 @@ impl WebviewAttributes { self.scroll_bar_style = style; self } + + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.general_autofill_enabled = enabled; + self + } } /// IPC handler. diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs index 790910a3cc58..de911f255b1a 100644 --- a/crates/tauri-runtime/src/window.rs +++ b/crates/tauri-runtime/src/window.rs @@ -62,6 +62,26 @@ pub enum WindowEvent { /// /// Applications might wish to react to this to change the theme of the content of the window when the system changes the window theme. ThemeChanged(Theme), + + /// Emitted when the application has been suspended. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onPause` method of the Activity. + /// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Suspended, + + /// Emitted when the application has been resumed. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation. + /// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Resumed, } /// An event from a window. @@ -477,6 +497,23 @@ pub trait WindowBuilder: WindowBuilderBase { /// Sets custom name for Windows' window class. **Windows only**. #[must_use] fn window_classname>(self, window_classname: S) -> Self; + + /// The name of the activity to create for this webview window. + #[cfg(target_os = "android")] + fn activity_name>(self, class_name: S) -> Self; + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + #[cfg(target_os = "android")] + fn created_by_activity_name>(self, class_name: S) -> Self; + + /// Sets the identifier of the UIScene that is requesting the creation of this new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(self, identifier: S) -> Self; } /// A window that has yet to be built. diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index cbde416db413..75c405c6c21e 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -1,5 +1,5 @@ { - "$id": "https://schema.tauri.app/config/2.10.3", + "$id": "https://schema.tauri.app/config/2.11.1", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "The Tauri configuration object.\nIt is read from a file where you can define your frontend assets,\nconfigure the bundler and define a tray icon.\n\nThe configuration file is generated by the\n[`tauri init`](https://v2.tauri.app/reference/cli/#init) command that lives in\nyour Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\nThe JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\nThe TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can\nread a platform-specific configuration from `tauri.linux.conf.json`,\n`tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n(or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\nwhich gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`app`](#appconfig): The Tauri configuration\n- [`build`](#buildconfig): The build configuration\n- [`bundle`](#bundleconfig): The bundle configurations\n- [`plugins`](#pluginconfig): The plugins configuration\n\nExample tauri.config.json file:\n\n```json\n{\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"http://localhost:3000\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n}\n```", @@ -68,7 +68,10 @@ "description": "The build configuration.", "default": { "removeUnusedCommands": false, - "additionalWatchFolders": [] + "additionalWatchFolders": [], + "windows": { + "staticVCRuntime": true + } }, "allOf": [ { @@ -94,9 +97,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "linux": { "appimage": { @@ -549,7 +554,7 @@ ] }, "backgroundThrottling": { - "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578", + "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee ", "anyOf": [ { "$ref": "#/definitions/BackgroundThrottlingPolicy" @@ -604,6 +609,32 @@ "$ref": "#/definitions/ScrollBarStyle" } ] + }, + "activityName": { + "description": "The name of the Android activity to create for this window.", + "type": [ + "string", + "null" + ] + }, + "createdByActivityName": { + "description": "The name of the Android activity that is creating this webview window.\n\nThis is important to determine which stack the activity will belong to.", + "type": [ + "string", + "null" + ] + }, + "requestedBySceneIdentifier": { + "description": "Sets the identifier of the scene that is requesting the new scene,\nestablishing a relationship between the two scenes.\n\nBy default the system uses the foreground scene.", + "type": [ + "string", + "null" + ] + }, + "generalAutofillEnabled": { + "description": "Controls the WebView's browser-level general autofill behavior.\n\n**This option does not disable password or credit card autofill.**\n\nWhen set to `false`, the WebView will not automatically populate\ngeneral form fields using previously stored data such as addresses\nor contact information.\n\nIf not specified, this is `true` by default.\n\n## Platform-specific\n\n- **Windows**: Supported. WebView2's autofill feature (called\n \"Suggestions\") may not honor `autocomplete=\"off\"` on input\n elements in some cases.\n- **Linux / Android / iOS / macOS**: Unsupported and performs no\n operation.", + "type": "boolean", + "default": true } }, "additionalProperties": false @@ -1068,7 +1099,7 @@ "const": "default" }, { - "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541", + "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee ", "type": "string", "const": "fluentOverlay" } @@ -1863,6 +1894,17 @@ "type": "string" }, "default": [] + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -1990,6 +2032,18 @@ } ] }, + "WindowsBuildConfig": { + "description": "Windows-specific build configuration.", + "type": "object", + "properties": { + "staticVCRuntime": { + "description": "Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets.", + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\nSee more: ", "type": "object", @@ -2129,9 +2183,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "allOf": [ { @@ -2370,7 +2426,7 @@ ] }, "mimeType": { - "description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.", + "description": "The mime-type of the association, e.g. `'image/png'` or `'text/plain'`.\n\n- **Linux**: written as `MimeType=` in the `.desktop` file.\n- **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of\n the `UTExportedTypeDeclarations` entry in `Info.plist`.\n- **Android**: used as `android:mimeType` in the `` element of an ``\n in `AndroidManifest.xml`.", "type": [ "string", "null" @@ -2395,6 +2451,16 @@ "type": "null" } ] + }, + "androidIntentActionFilters": { + "description": "Intent action filters for this file association.\n\nBy default all filters are used.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AndroidIntentAction" + } } }, "additionalProperties": false, @@ -2485,6 +2551,26 @@ "identifier" ] }, + "AndroidIntentAction": { + "description": "Android intent action.", + "oneOf": [ + { + "description": "ACTION_SEND.\n\n", + "type": "string", + "const": "send" + }, + { + "description": "ACTION_SEND_MULTIPLE.\n\n", + "type": "string", + "const": "sendMultiple" + }, + { + "description": "ACTION_VIEW.\n\n", + "type": "string", + "const": "view" + } + ] + }, "WindowsConfig": { "description": "Windows bundler configuration.\n\nSee more: ", "type": "object", @@ -2532,6 +2618,13 @@ "type": "boolean", "default": true }, + "minimumWebview2Version": { + "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "type": [ + "string", + "null" + ] + }, "wix": { "description": "Configuration for the MSI generated with WiX.", "anyOf": [ @@ -2564,6 +2657,11 @@ "type": "null" } ] + }, + "bundleVCRuntime": { + "description": "Whether to bundle the Visual C++ runtime DLLs alongside the application.\n\nThis can be particularly useful when your application includes sidecars or DLLs that do\nnot statically link the Visual C++ runtime and require the runtime DLLs at runtime, and\nyou do not want to require users to install the Visual C++ Redistributable. This can also\nbe useful when `build > windows > staticVCRuntime` is set to `false`.", + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -2842,6 +2940,20 @@ "null" ] }, + "uninstallerIcon": { + "description": "The path to an icon file used as the uninstaller icon.", + "type": [ + "string", + "null" + ] + }, + "uninstallerHeaderImage": { + "description": "The path to a bitmap file to display on the header of uninstallers pages.\nDefaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`\n\nThe recommended dimensions are 150px x 57px.", + "type": [ + "string", + "null" + ] + }, "installMode": { "description": "Whether the installation will be for all users or just the current user.", "default": "currentUser", @@ -2852,7 +2964,7 @@ ] }, "languages": { - "description": "A list of installer languages.\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", + "description": "A list of installer languages. Default to `[\"English\"]` if not set.\n\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", "type": [ "array", "null" @@ -2862,7 +2974,7 @@ } }, "customLanguageFiles": { - "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,", + "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array,", "type": [ "object", "null" @@ -2900,11 +3012,12 @@ ] }, "minimumWebview2Version": { - "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "description": "Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead.\n\nTry to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", "type": [ "string", "null" - ] + ], + "deprecated": true } }, "additionalProperties": false @@ -3685,6 +3798,13 @@ "description": "Whether to automatically increment the `versionCode` on each build.\n\n- If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n- If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\nNote that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.", "type": "boolean", "default": false + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\nThis allows installing debug and release versions side-by-side on the same device.\nExample: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-schema-worker/Cargo.toml b/crates/tauri-schema-worker/Cargo.toml index 47e9ce1e124e..7c407c2fb6bc 100644 --- a/crates/tauri-schema-worker/Cargo.toml +++ b/crates/tauri-schema-worker/Cargo.toml @@ -8,8 +8,8 @@ publish = false crate-type = ["cdylib"] [dependencies] -worker = { version = "0.7", features = ['http', 'axum'] } -worker-macros = { version = "0.7", features = ['http'] } +worker = { version = "0.8", features = ['http', 'axum'] } +worker-macros = { version = "0.8", features = ['http'] } console_error_panic_hook = { version = "0.1" } axum = { version = "0.8", default-features = false } tower-service = "0.3" diff --git a/crates/tauri-schema-worker/package.json b/crates/tauri-schema-worker/package.json index 7e7326a0dfb3..c5cba019328e 100644 --- a/crates/tauri-schema-worker/package.json +++ b/crates/tauri-schema-worker/package.json @@ -8,6 +8,6 @@ "dev": "wrangler dev" }, "devDependencies": { - "wrangler": "^4.70.0" + "wrangler": "^4.75.0" } } diff --git a/crates/tauri-schema-worker/src/config.rs b/crates/tauri-schema-worker/src/config.rs index 3145cd65e3a0..5812743ea080 100644 --- a/crates/tauri-schema-worker/src/config.rs +++ b/crates/tauri-schema-worker/src/config.rs @@ -6,8 +6,8 @@ use anyhow::Context; use axum::{ Router, extract::Path, - http::{StatusCode, header}, - response::Result, + http::{HeaderValue, StatusCode, header}, + response::{IntoResponse, Result}, routing::get, }; use semver::{Version, VersionReq}; @@ -48,23 +48,26 @@ pub fn router() -> Router { .route("/config/{version}", get(schema_for_version)) } -async fn schema_for_version(Path(version): Path) -> Result { +async fn schema_for_version(Path(version): Path) -> Result { try_schema_for_version(version) .await + .map(JsonResponse) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) .map_err(Into::into) } -async fn stable_schema() -> Result { +async fn stable_schema() -> Result { try_stable_schema() .await + .map(JsonResponse) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) .map_err(Into::into) } -async fn next_schema() -> Result { +async fn next_schema() -> Result { try_next_schema() .await + .map(JsonResponse) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) .map_err(Into::into) } @@ -172,3 +175,18 @@ fn fetch_req(url: &str) -> anyhow::Result { ) .map_err(Into::into) } + +struct JsonResponse(String); + +impl IntoResponse for JsonResponse { + fn into_response(self) -> axum::response::Response { + ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + )], + self.0, + ) + .into_response() + } +} diff --git a/crates/tauri-schema-worker/wrangler.toml b/crates/tauri-schema-worker/wrangler.toml index 661b6059ab28..e7e56a635766 100644 --- a/crates/tauri-schema-worker/wrangler.toml +++ b/crates/tauri-schema-worker/wrangler.toml @@ -9,7 +9,7 @@ send_metrics = false # The minor version of worker-build must match worker/worker-macros in Cargo.toml! [build] -command = "cargo install -q worker-build@^0.7 && worker-build --release" +command = "cargo install -q worker-build@^0.8 && worker-build --release" [observability] enabled = true diff --git a/crates/tauri-utils/CHANGELOG.md b/crates/tauri-utils/CHANGELOG.md index 4ed8a4248dcb..dc7ed0537832 100644 --- a/crates/tauri-utils/CHANGELOG.md +++ b/crates/tauri-utils/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## \[2.9.1] + +### Dependencies + +- [`4f548e739`](https://www.github.com/tauri-apps/tauri/commit/4f548e73947b3b06bf2073c822564aed3dd5f948) ([#15308](https://www.github.com/tauri-apps/tauri/pull/15308)) Updated `phf` to 0.13 + +## \[2.9.0] + +### New Features + +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). + +### Dependencies + +- [`e032c3b34`](https://www.github.com/tauri-apps/tauri/commit/e032c3b3421f53bca7b869ffee2be105c5c06ad9) ([#14959](https://www.github.com/tauri-apps/tauri/pull/14959)) Add new `html-manipulation-2` and `build-2` feature flags that use `dom_query` instead of `kuchikiki` for HTML parsing / manipulation. + This allows downstream users to remove `kuchikiki` and its dependencies from their dependency tree. +- [`1ef6a119b`](https://www.github.com/tauri-apps/tauri/commit/1ef6a119b1571d1da0acc08bdb7fd5521a4c6d52) ([#15115](https://www.github.com/tauri-apps/tauri/pull/15115)) Changed `toml` crate version from `0.9` to `">=0.9, <=1"` + ## \[2.8.3] ### Bug Fixes diff --git a/crates/tauri-utils/Cargo.toml b/crates/tauri-utils/Cargo.toml index ed6bc6c18d5e..190753ce3593 100644 --- a/crates/tauri-utils/Cargo.toml +++ b/crates/tauri-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-utils" -version = "2.8.3" +version = "2.9.1" description = "Utilities for Tauri" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -17,13 +17,14 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1" thiserror = "2" -phf = { version = "0.11", features = ["macros"] } +phf = { version = "0.13", features = ["macros"] } brotli = { version = "8", optional = true, default-features = false, features = [ "std", ] } url = { version = "2", features = ["serde"] } html5ever = { version = "0.29", optional = true } kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader", optional = true } +dom_query = { version = "0.27", optional = true, default-features = false } proc-macro2 = { version = "1", optional = true } quote = { version = "1", optional = true } schemars = { version = "1", features = ["url2", "uuid1"], optional = true } @@ -31,9 +32,13 @@ serde_with = "3" aes-gcm = { version = "0.10", optional = true } getrandom = { version = "0.3", optional = true, features = ["std"] } serialize-to-javascript = { version = "0.1.2", optional = true } -ctor = "0.2" +ctor = { version = "0.8", default-features = false, features = [ + "std", + "proc_macro", +] } json5 = { version = "0.4", optional = true } -toml = { version = "0.9", features = ["parse"] } +# Part of public api in error type +toml = { version = ">=0.9, <=1", features = ["parse"] } json-patch = "3.0" # Our code requires at least 0.3.1 glob = "0.3.1" @@ -49,6 +54,7 @@ cargo_metadata = { version = "0.19", optional = true } serde-untagged = "0.1" uuid = { version = "1", features = ["serde"] } http = "1" +plist = "1" [target."cfg(target_os = \"macos\")".dependencies] swift-rs = { version = "1", optional = true, features = ["build"] } @@ -57,6 +63,7 @@ swift-rs = { version = "1", optional = true, features = ["build"] } getrandom = { version = "0.3", features = ["std"] } serial_test = "3" tauri = { path = "../tauri" } +tempfile = "3.15.0" [features] build = [ @@ -67,6 +74,15 @@ build = [ "swift-rs", "html-manipulation", ] +# Same as `build` but uses `html-manipulation-2` to avoid the `kuchikiki` dependency. +build-2 = [ + "proc-macro2", + "quote", + "cargo_metadata", + "schema", + "swift-rs", + "html-manipulation-2", +] compression = ["brotli"] schema = ["schemars"] isolation = ["aes-gcm", "getrandom", "serialize-to-javascript"] @@ -75,3 +91,4 @@ config-json5 = ["json5"] config-toml = [] resources = ["walkdir"] html-manipulation = ["dep:html5ever", "dep:kuchiki"] +html-manipulation-2 = ["dep:dom_query"] diff --git a/crates/tauri-utils/src/acl/build.rs b/crates/tauri-utils/src/acl/build.rs index 6667e9fa2c98..d634f82f6d11 100644 --- a/crates/tauri-utils/src/acl/build.rs +++ b/crates/tauri-utils/src/acl/build.rs @@ -463,8 +463,8 @@ pub fn generate_allowed_commands( let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?; let permission_entries = capabilities - .into_iter() - .flat_map(|(_, capabilities)| capabilities.permissions); + .into_values() + .flat_map(|capabilities| capabilities.permissions); let mut allowed_commands = AllowedCommands { has_app_acl: has_app_manifest(&acl), ..Default::default() diff --git a/crates/tauri-utils/src/acl/capability.rs b/crates/tauri-utils/src/acl/capability.rs index 5fa6435e19de..e870678fe697 100644 --- a/crates/tauri-utils/src/acl/capability.rs +++ b/crates/tauri-utils/src/acl/capability.rs @@ -317,7 +317,7 @@ impl FromStr for CapabilityFile { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use std::convert::identity; diff --git a/crates/tauri-utils/src/acl/identifier.rs b/crates/tauri-utils/src/acl/identifier.rs index 1ceea7166b9b..e2c4b3d78222 100644 --- a/crates/tauri-utils/src/acl/identifier.rs +++ b/crates/tauri-utils/src/acl/identifier.rs @@ -284,7 +284,7 @@ mod tests { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; diff --git a/crates/tauri-utils/src/acl/manifest.rs b/crates/tauri-utils/src/acl/manifest.rs index 64624edafcf2..d71c61223024 100644 --- a/crates/tauri-utils/src/acl/manifest.rs +++ b/crates/tauri-utils/src/acl/manifest.rs @@ -132,7 +132,7 @@ impl Manifest { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; diff --git a/crates/tauri-utils/src/acl/mod.rs b/crates/tauri-utils/src/acl/mod.rs index 3b48bb6a73a9..509211e6cca1 100644 --- a/crates/tauri-utils/src/acl/mod.rs +++ b/crates/tauri-utils/src/acl/mod.rs @@ -58,7 +58,7 @@ pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json"; /// the value is set to the config's directory pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS"; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub mod build; pub mod capability; pub mod identifier; @@ -108,7 +108,7 @@ pub enum Error { CreateDir(std::io::Error, PathBuf), /// [`cargo_metadata`] was not able to complete successfully - #[cfg(feature = "build")] + #[cfg(any(feature = "build", feature = "build-2"))] #[error("failed to execute: {0}")] Metadata(#[from] ::cargo_metadata::Error), @@ -464,7 +464,7 @@ mod tests { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build_ { use std::convert::identity; diff --git a/crates/tauri-utils/src/acl/resolved.rs b/crates/tauri-utils/src/acl/resolved.rs index b30fb6677550..e9fc52393934 100644 --- a/crates/tauri-utils/src/acl/resolved.rs +++ b/crates/tauri-utils/src/acl/resolved.rs @@ -438,7 +438,7 @@ fn display_perm_key(prefix: &str) -> &str { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; diff --git a/crates/tauri-utils/src/acl/value.rs b/crates/tauri-utils/src/acl/value.rs index f0f91a891701..a74bee5a5963 100644 --- a/crates/tauri-utils/src/acl/value.rs +++ b/crates/tauri-utils/src/acl/value.rs @@ -145,7 +145,7 @@ impl From for Value { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use std::convert::identity; diff --git a/crates/tauri-utils/src/assets.rs b/crates/tauri-utils/src/assets.rs index 3d904cd1f39f..486662d8dce8 100644 --- a/crates/tauri-utils/src/assets.rs +++ b/crates/tauri-utils/src/assets.rs @@ -168,27 +168,18 @@ impl EmbeddedAssets { /// Get an asset by key. #[cfg(feature = "compression")] pub fn get(&self, key: &AssetKey) -> Option> { - self - .assets - .get(key.as_ref()) - .map(|&(mut asdf)| { - // with the exception of extremely small files, output should usually be - // at least as large as the compressed version. - let mut buf = Vec::with_capacity(asdf.len()); - brotli::BrotliDecompress(&mut asdf, &mut buf).map(|()| buf) - }) - .and_then(Result::ok) - .map(Cow::Owned) + let &(mut asdf) = self.assets.get(key.as_ref())?; + // with the exception of extremely small files, output should usually be + // at least as large as the compressed version. + let mut buf = Vec::with_capacity(asdf.len()); + brotli::BrotliDecompress(&mut asdf, &mut buf).ok()?; + Some(Cow::Owned(buf)) } /// Get an asset by key. #[cfg(not(feature = "compression"))] pub fn get(&self, key: &AssetKey) -> Option> { - self - .assets - .get(key.as_ref()) - .copied() - .map(|a| Cow::Owned(a.to_vec())) + Some(Cow::Borrowed(self.assets.get(key.as_ref())?)) } /// Iterate on the assets. diff --git a/crates/tauri-utils/src/build.rs b/crates/tauri-utils/src/build.rs index 57456b45278f..de575ab4a47e 100644 --- a/crates/tauri-utils/src/build.rs +++ b/crates/tauri-utils/src/build.rs @@ -98,3 +98,74 @@ fn link_xcode_library(name: &str, source: impl AsRef) { println!("cargo:rustc-link-search=native={}", lib_out_dir.display()); println!("cargo:rustc-link-lib=static={name}"); } + +/// Updates the Android manifest by inserting XML content into a specified parent tag. +/// +/// The content is wrapped in auto-generated comments and will replace any existing +/// content with the same block identifier. +/// +/// # Arguments +/// +/// * `block_identifier` - A unique identifier for the block (used in comments) +/// * `parent` - The parent XML tag name (e.g., "activity", "application") +/// * `insert` - The XML content to insert +pub fn update_android_manifest( + block_identifier: &str, + parent: &str, + insert: String, +) -> anyhow::Result<()> { + use std::{ + env::var_os, + fs::{read_to_string, write}, + path::PathBuf, + }; + + if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + let manifest_path = project_path.join("app/src/main/AndroidManifest.xml"); + if !manifest_path.exists() { + return Ok(()); + } + let manifest = read_to_string(&manifest_path)?; + let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert); + if rewritten != manifest { + write(&manifest_path, rewritten)?; + } + } + Ok(()) +} + +fn xml_block_comment(id: &str) -> String { + format!("") +} + +fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String { + let block_comment = xml_block_comment(block_identifier); + + let mut rewritten = Vec::new(); + let mut found_block = false; + let parent_closing_tag = format!(""); + for line in xml.split('\n') { + if line.contains(&block_comment) { + found_block = !found_block; + continue; + } + + // found previous block which should be removed + if found_block { + continue; + } + + if let Some(index) = line.find(&parent_closing_tag) { + let indentation = " ".repeat(index + 4); + rewritten.push(format!("{indentation}{block_comment}")); + for l in contents.split('\n') { + rewritten.push(format!("{indentation}{l}")); + } + rewritten.push(format!("{indentation}{block_comment}")); + } + + rewritten.push(line.to_string()); + } + + rewritten.join("\n") +} diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index c5c8f8a53085..9bce2685e8cf 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -36,7 +36,7 @@ use serde_with::skip_serializing_none; use url::Url; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::{self, Display}, fs::read_to_string, path::PathBuf, @@ -810,13 +810,24 @@ pub struct NsisConfig { /// The recommended dimensions are 164px x 314px. #[serde(alias = "sidebar-image")] pub sidebar_image: Option, + // TODO: Change the alias to installer-icon in v3 /// The path to an icon file used as the installer icon. #[serde(alias = "install-icon")] pub installer_icon: Option, + /// The path to an icon file used as the uninstaller icon. + #[serde(alias = "uninstaller-icon")] + pub uninstaller_icon: Option, + /// The path to a bitmap file to display on the header of uninstallers pages. + /// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image` + /// + /// The recommended dimensions are 150px x 57px. + #[serde(alias = "uninstaller-header-image")] + pub uninstaller_header_image: Option, /// Whether the installation will be for all users or just the current user. #[serde(default, alias = "install-mode")] pub install_mode: NSISInstallerMode, - /// A list of installer languages. + /// A list of installer languages. Default to `["English"]` if not set. + /// /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. /// To allow the user to select the language, set `display_language_selector` to `true`. /// @@ -827,7 +838,7 @@ pub struct NsisConfig { /// /// See for an example `.nsh` file. /// - /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array, + /// **Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array, pub custom_language_files: Option>, /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. /// By default the OS language is selected, with a fallback to the first language in the `languages` array. @@ -879,9 +890,15 @@ pub struct NsisConfig { /// ``` #[serde(alias = "installer-hooks")] pub installer_hooks: Option, + /// Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead. + /// /// Try to ensure that the WebView2 version is equal to or newer than this version, /// if the user's WebView2 is older than this version, /// the installer will try to trigger a WebView2 update. + #[deprecated( + since = "2.10.0", + note = "Use `WindowsConfig::minimum_webview2_version` instead." + )] #[serde(alias = "minimum-webview2-version")] pub minimum_webview2_version: Option, } @@ -996,6 +1013,11 @@ pub struct WindowsConfig { /// The default value of this flag is `true`. #[serde(default = "default_true", alias = "allow-downgrades")] pub allow_downgrades: bool, + /// Try to ensure that the WebView2 version is equal to or newer than this version, + /// if the user's WebView2 is older than this version, + /// the installer will try to trigger a WebView2 update. + #[serde(alias = "minimum-webview2-version")] + pub minimum_webview2_version: Option, /// Configuration for the MSI generated with WiX. pub wix: Option, /// Configuration for the installer generated with NSIS. @@ -1009,6 +1031,19 @@ pub struct WindowsConfig { /// need to use another tool like `osslsigncode`. #[serde(alias = "sign-command")] pub sign_command: Option, + /// Whether to bundle the Visual C++ runtime DLLs alongside the application. + /// + /// This can be particularly useful when your application includes sidecars or DLLs that do + /// not statically link the Visual C++ runtime and require the runtime DLLs at runtime, and + /// you do not want to require users to install the Visual C++ Redistributable. This can also + /// be useful when `build > windows > staticVCRuntime` is set to `false`. + #[serde( + default, + rename = "bundleVCRuntime", + alias = "bundle-vc-runtime", + alias = "bundleVcRuntime" + )] + pub bundle_vc_runtime: bool, } impl Default for WindowsConfig { @@ -1020,9 +1055,11 @@ impl Default for WindowsConfig { tsp: false, webview_install_mode: Default::default(), allow_downgrades: true, + minimum_webview2_version: None, wix: None, nsis: None, sign_command: None, + bundle_vc_runtime: false, } } } @@ -1128,7 +1165,13 @@ pub struct FileAssociation { /// The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. #[serde(default)] pub role: BundleTypeRole, - /// The mime-type e.g. 'image/png' or 'text/plain'. Linux-only. + /// The mime-type of the association, e.g. `'image/png'` or `'text/plain'`. + /// + /// - **Linux**: written as `MimeType=` in the `.desktop` file. + /// - **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of + /// the `UTExportedTypeDeclarations` entry in `Info.plist`. + /// - **Android**: used as `android:mimeType` in the `` element of an `` + /// in `AndroidManifest.xml`. #[serde(alias = "mime-type")] pub mime_type: Option, /// The ranking of this app among apps that declare themselves as editors or viewers of the given file type. Maps to `LSHandlerRank` on macOS. @@ -1138,6 +1181,31 @@ pub struct FileAssociation { /// /// You should define this if the associated file is a custom file type defined by your application. pub exported_type: Option, + /// Intent action filters for this file association. + /// + /// By default all filters are used. + #[serde(alias = "android-intent-action-filters")] + pub android_intent_action_filters: Option>, +} + +/// Android intent action. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Hash)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub enum AndroidIntentAction { + /// ACTION_SEND. + /// + /// + Send, + /// ACTION_SEND_MULTIPLE. + /// + /// + SendMultiple, + /// ACTION_VIEW. + /// + /// + View, } /// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS. @@ -1154,6 +1222,227 @@ pub struct ExportedFileAssociation { pub conforms_to: Option>, } +impl FileAssociation { + /// Infers UTIs (Uniform Type Identifiers) from file extensions and mime types. + /// This is useful for macOS and iOS to automatically populate `LSItemContentTypes` + /// in the Info.plist for share sheet and file association support. + /// + /// Returns a vector of UTIs that should be included in `LSItemContentTypes`. + /// Explicitly provided content types are included first, followed by inferred types. + pub fn infer_content_types(&self) -> HashSet { + let mut content_types = HashSet::new(); + + // when we have an exported type, we only reference it + if let Some(exported_type) = &self.exported_type { + content_types.insert(exported_type.identifier.clone()); + return content_types; + } + + // Start with explicitly provided content types + if let Some(explicit_types) = &self.content_types { + content_types.extend(explicit_types.iter().cloned()); + } + + // Infer from extensions and add to content_types (avoiding duplicates) + for ext in &self.ext { + if let Some(uti) = extension_to_uti(&ext.0) { + content_types.insert(uti.to_string()); + } + } + + // Also infer from mime type if available (avoiding duplicates) + if let Some(mime_type) = &self.mime_type + && let Some(uti) = mime_type_to_uti(mime_type) + { + content_types.insert(uti.to_string()); + } + + content_types + } +} + +/// Generates plist dictionary entries for file associations. +/// This is used by both macOS and iOS bundlers to populate Info.plist. +/// +/// Returns a plist dictionary containing `UTExportedTypeDeclarations` and `CFBundleDocumentTypes` +/// if there are any file associations configured. +pub fn file_associations_plist(associations: &[FileAssociation]) -> Option { + use plist::{Dictionary, Value}; + + if associations.is_empty() { + return None; + } + + let exported_associations = associations + .iter() + .filter_map(|association| { + association.exported_type.as_ref().map(|exported_type| { + let mut dict = Dictionary::new(); + + dict.insert( + "UTTypeIdentifier".into(), + exported_type.identifier.clone().into(), + ); + if let Some(description) = &association.description { + dict.insert("UTTypeDescription".into(), description.clone().into()); + } + if let Some(conforms_to) = &exported_type.conforms_to { + dict.insert( + "UTTypeConformsTo".into(), + Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()), + ); + } + + let mut specification = Dictionary::new(); + specification.insert( + "public.filename-extension".into(), + Value::Array( + association + .ext + .iter() + .map(|s| s.to_string().into()) + .collect(), + ), + ); + if let Some(mime_type) = &association.mime_type { + specification.insert("public.mime-type".into(), mime_type.clone().into()); + } + + dict.insert("UTTypeTagSpecification".into(), specification.into()); + + Value::Dictionary(dict) + }) + }) + .collect::>(); + + let document_types = associations + .iter() + .map(|association| { + let mut dict = Dictionary::new(); + + if !association.ext.is_empty() { + dict.insert( + "CFBundleTypeExtensions".into(), + Value::Array( + association + .ext + .iter() + .map(|ext| ext.to_string().into()) + .collect(), + ), + ); + } + + // For macOS/iOS share sheet, we need LSItemContentTypes with standard UTIs + let content_types = association.infer_content_types(); + + // Add LSItemContentTypes if we have any content types + if !content_types.is_empty() { + dict.insert( + "LSItemContentTypes".into(), + Value::Array(content_types.iter().map(|s| s.clone().into()).collect()), + ); + } + + let type_name = association + .name + .clone() + .or_else(|| association.ext.first().map(|ext| ext.0.clone())) + .unwrap_or_default(); + dict.insert("CFBundleTypeName".into(), type_name.into()); + dict.insert( + "CFBundleTypeRole".into(), + association.role.to_string().into(), + ); + dict.insert("LSHandlerRank".into(), association.rank.to_string().into()); + + Value::Dictionary(dict) + }) + .collect::>(); + + if exported_associations.is_empty() && document_types.is_empty() { + return None; + } + + let mut plist = Dictionary::new(); + if !exported_associations.is_empty() { + plist.insert( + "UTExportedTypeDeclarations".into(), + Value::Array(exported_associations), + ); + } + if !document_types.is_empty() { + plist.insert("CFBundleDocumentTypes".into(), Value::Array(document_types)); + } + + Some(Value::Dictionary(plist)) +} + +/// Maps file extensions to their standard UTIs for macOS/iOS share sheet support +fn extension_to_uti(ext: &str) -> Option<&'static str> { + match ext.to_lowercase().as_str() { + // Images + "png" => Some("public.png"), + "jpg" | "jpeg" => Some("public.jpeg"), + "gif" => Some("com.compuserve.gif"), + "bmp" => Some("com.microsoft.bmp"), + "tiff" | "tif" => Some("public.tiff"), + "ico" => Some("com.microsoft.ico"), + "heic" | "heif" => Some("public.heif-standard-image"), + "webp" => Some("org.webmproject.webp"), + "svg" => Some("public.svg-image"), + // Videos + "mp4" => Some("public.mpeg-4"), + "mov" => Some("com.apple.quicktime-movie"), + "avi" => Some("public.avi"), + "mkv" => Some("public.mpeg-4"), + // Audio + "mp3" => Some("public.mp3"), + "wav" => Some("com.microsoft.waveform-audio"), + "aac" => Some("public.aac-audio"), + "m4a" => Some("public.mpeg-4-audio"), + // Documents + "pdf" => Some("com.adobe.pdf"), + "txt" => Some("public.plain-text"), + "rtf" => Some("public.rtf"), + "html" | "htm" => Some("public.html"), + "json" => Some("public.json"), + "xml" => Some("public.xml"), + _ => None, + } +} + +/// Infers UTIs from mime type +fn mime_type_to_uti(mime_type: &str) -> Option<&'static str> { + match mime_type { + "image/png" => Some("public.png"), + "image/jpeg" | "image/jpg" => Some("public.jpeg"), + "image/gif" => Some("com.compuserve.gif"), + "image/bmp" => Some("com.microsoft.bmp"), + "image/tiff" => Some("public.tiff"), + "image/heic" | "image/heif" => Some("public.heif-standard-image"), + "image/webp" => Some("org.webmproject.webp"), + "image/svg+xml" => Some("public.svg-image"), + mime if mime.starts_with("image/") => Some("public.image"), + "video/mp4" => Some("public.mpeg-4"), + "video/quicktime" => Some("com.apple.quicktime-movie"), + "video/x-msvideo" => Some("public.avi"), + mime if mime.starts_with("video/") => Some("public.movie"), + "audio/mpeg" | "audio/mp3" => Some("public.mp3"), + "audio/wav" | "audio/wave" => Some("com.microsoft.waveform-audio"), + "audio/aac" => Some("public.aac-audio"), + "audio/mp4" => Some("public.mpeg-4-audio"), + mime if mime.starts_with("audio/") => Some("public.audio"), + "application/pdf" => Some("com.adobe.pdf"), + "text/plain" => Some("public.plain-text"), + "text/rtf" => Some("public.rtf"), + "text/html" => Some("public.html"), + "application/json" => Some("public.json"), + "application/xml" | "text/xml" => Some("public.xml"), + _ => None, + } +} + /// Deep link protocol configuration. #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] @@ -1567,7 +1856,7 @@ pub enum ScrollBarStyle { /// Fluent UI style overlay scrollbars. **Windows Only** /// /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + /// see FluentOverlay, } @@ -1859,7 +2148,7 @@ pub struct WindowConfig { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[serde(default, alias = "background-throttling")] pub background_throttling: Option, /// Whether we should disable JavaScript code execution on the webview or not. @@ -1919,6 +2208,40 @@ pub struct WindowConfig { /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. #[serde(default, alias = "scroll-bar-style")] pub scroll_bar_style: ScrollBarStyle, + /// The name of the Android activity to create for this window. + #[serde(default, alias = "activity-name")] + pub activity_name: Option, + /// The name of the Android activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + #[serde(default, alias = "created-by-activity-name")] + pub created_by_activity_name: Option, + + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[serde(default, alias = "requested-by-scene-identifier")] + pub requested_by_scene_identifier: Option, + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// If not specified, this is `true` by default. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[serde(default = "default_true", alias = "general-autofill-enabled")] + pub general_autofill_enabled: bool, } impl Default for WindowConfig { @@ -1981,6 +2304,10 @@ impl Default for WindowConfig { data_directory: None, data_store_identifier: None, scroll_bar_style: ScrollBarStyle::Default, + activity_name: None, + created_by_activity_name: None, + requested_by_scene_identifier: None, + general_autofill_enabled: true, } } } @@ -1990,11 +2317,11 @@ fn default_window_label() -> String { } fn default_width() -> f64 { - 800f64 + 800. } fn default_height() -> f64 { - 600f64 + 600. } fn default_title() -> String { @@ -2858,6 +3185,12 @@ pub struct AndroidConfig { /// Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository. #[serde(alias = "auto-increment-version-code", default)] pub auto_increment_version_code: bool, + + /// Application ID suffix to append for debug builds. + /// This allows installing debug and release versions side-by-side on the same device. + /// Example: ".debug" will make debug builds use "com.example.app.debug" as the application ID. + #[serde(alias = "debug-application-id-suffix")] + pub debug_application_id_suffix: Option, } impl Default for AndroidConfig { @@ -2866,6 +3199,7 @@ impl Default for AndroidConfig { min_sdk_version: default_min_sdk_version(), version_code: None, auto_increment_version_code: false, + debug_application_id_suffix: None, } } } @@ -3067,6 +3401,32 @@ pub struct BuildConfig { /// Additional paths to watch for changes when running `tauri dev`. #[serde(alias = "additional-watch-directories", default)] pub additional_watch_folders: Vec, + /// Windows-specific build configuration. + #[serde(default)] + pub windows: WindowsBuildConfig, +} + +/// Windows-specific build configuration. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WindowsBuildConfig { + /// Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets. + #[serde( + default = "default_true", + rename = "staticVCRuntime", + alias = "static-vc-runtime", + alias = "staticVcRuntime" + )] + pub static_vc_runtime: bool, +} + +impl Default for WindowsBuildConfig { + fn default() -> Self { + Self { + static_vc_runtime: true, + } + } } #[derive(Debug, PartialEq, Eq)] @@ -3275,7 +3635,7 @@ pub struct PluginConfig(pub HashMap); /// This allows for a build script to output the values in a `Config` to a `TokenStream`, which can /// then be consumed by another crate. Useful for passing a config to both the build script and the /// application using tauri while only parsing it once (in the build script). -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use super::*; use crate::{literal_struct, tokens::*}; @@ -3515,6 +3875,10 @@ mod build { let data_directory = opt_lit(self.data_directory.as_ref().map(path_buf_lit).as_ref()); let data_store_identifier = opt_vec_lit(self.data_store_identifier, identity); let scroll_bar_style = &self.scroll_bar_style; + let activity_name = opt_lit(self.activity_name.as_ref()); + let created_by_activity_name = opt_lit(self.created_by_activity_name.as_ref()); + let requested_by_scene_identifier = opt_lit(self.requested_by_scene_identifier.as_ref()); + let general_autofill_enabled = self.general_autofill_enabled; literal_struct!( tokens, @@ -3575,7 +3939,11 @@ mod build { disable_input_accessory_view, data_directory, data_store_identifier, - scroll_bar_style + scroll_bar_style, + activity_name, + created_by_activity_name, + requested_by_scene_identifier, + general_autofill_enabled ); } } @@ -3737,6 +4105,7 @@ mod build { let features = quote!(None); let remove_unused_commands = quote!(false); let additional_watch_folders = quote!(Vec::new()); + let windows = &self.windows; literal_struct!( tokens, @@ -3749,7 +4118,20 @@ mod build { before_bundle_command, features, remove_unused_commands, - additional_watch_folders + additional_watch_folders, + windows + ); + } + } + + impl ToTokens for WindowsBuildConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let static_vc_runtime = self.static_vc_runtime; + + literal_struct!( + tokens, + ::tauri::utils::config::WindowsBuildConfig, + static_vc_runtime ); } } @@ -4093,6 +4475,7 @@ mod test { features: None, remove_unused_commands: false, additional_watch_folders: Vec::new(), + windows: WindowsBuildConfig::default(), }; // create a bundle config diff --git a/crates/tauri-utils/src/html.rs b/crates/tauri-utils/src/html.rs index 4f8499798c53..cc5344d87077 100644 --- a/crates/tauri-utils/src/html.rs +++ b/crates/tauri-utils/src/html.rs @@ -281,6 +281,7 @@ pub fn inline_isolation(document: &NodeRef, dir: &Path) { } } +// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842 /// Normalize line endings in script content to match what the browser uses for CSP hashing. /// /// According to the HTML spec, browsers normalize: @@ -315,6 +316,13 @@ pub fn normalize_script_for_csp(input: &[u8]) -> Vec { #[cfg(test)] mod tests { + use std::io::Write; + + use super::*; + use crate::{ + assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config, + }; #[test] fn csp() { @@ -322,12 +330,14 @@ mod tests { "".to_string(), "".to_string(), ]; + for html in htmls { - let document = super::parse(html); + let document = parse(html); let csp = "csp-string"; - super::inject_csp(&document, csp); + inject_csp(&document, csp); + assert_eq!( - document.to_string(), + String::from_utf8(serialize_node(&document)).unwrap(), format!( r#""#, ) @@ -336,12 +346,97 @@ mod tests { } #[test] - fn normalize_script_for_csp() { + fn normalize_script_for_csp_test() { let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n"; let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n"; + + assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes()) + } + + #[test] + fn parse_and_serialize_roundtrips() { + let htmls = [ + "Test

Hello

", + "", + ]; + + for html in htmls { + let parsed = parse(html.to_string()); + let serialized = serialize_node(&parsed); + let result = String::from_utf8(serialized).unwrap(); + + assert_eq!(result, html); + } + } + + #[test] + fn inject_nonce_to_scripts() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_node(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn inject_nonce_to_styles() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_node(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn inject_nonce_skips_existing() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!(String::from_utf8(serialize_node(&document)).unwrap(), html); + } + + #[test] + fn inject_nonce_respects_disabled_modification() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true)); + assert_eq!( - super::normalize_script_for_csp(js.as_bytes()), - expected.as_bytes() - ) + String::from_utf8(serialize_node(&document)).unwrap(), + r#""# + ); + } + + #[test] + fn inline_isolation_replaces_src_with_content() { + let temp_dir = tempfile::tempdir().unwrap(); + let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap(); + file.write_all(b"console.log('test');").unwrap(); + let file_name = file.path().file_name().unwrap().to_str().unwrap(); + + let html = + format!(r#""#); + let document = parse(html); + inline_isolation(&document, temp_dir.path()); + + assert_eq!( + String::from_utf8(serialize_node(&document)).unwrap(), + r#""# + ); } } diff --git a/crates/tauri-utils/src/html2.rs b/crates/tauri-utils/src/html2.rs new file mode 100644 index 000000000000..7f591dc2788d --- /dev/null +++ b/crates/tauri-utils/src/html2.rs @@ -0,0 +1,335 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The module to process HTML in Tauri. +//! +//! # Stability +//! +//! This is utility used in Tauri internally and not considered part of the stable API. +//! If you use it, note that it may include breaking changes in the future. + +use dom_query::NodeRef; + +use crate::{ + assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config::DisabledCspModificationKind, +}; + +/// # Stability +/// +/// This dependency might receive updates in minor releases. +pub use dom_query::Document; + +/// Serializes the document to HTML. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn serialize_doc(document: &Document) -> Vec { + document.html().as_bytes().to_vec() +} + +/// Parses the given HTML string. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn parse_doc(html: String) -> Document { + Document::from(html) +} + +fn ensure_head(document: &Document) -> NodeRef<'_> { + document.head().unwrap_or_else(|| { + let html = document.html_root(); + let head = document.tree.new_element("head"); + html.prepend_child(&head); + head + }) +} + +fn inject_nonce(document: &Document, selector: &str, token: &str) { + let elements = document.select(selector); + for elem in elements.nodes() { + // if the node already has the `nonce` attribute, skip it + if elem.attr("nonce").is_some() { + continue; + } + elem.set_attr("nonce", token); + } +} + +/// Inject nonce tokens to all scripts and styles. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn inject_nonce_token( + document: &Document, + dangerous_disable_asset_csp_modification: &DisabledCspModificationKind, +) { + if dangerous_disable_asset_csp_modification.can_modify("script-src") { + inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN); + } + if dangerous_disable_asset_csp_modification.can_modify("style-src") { + inject_nonce(document, "style", STYLE_NONCE_TOKEN); + } +} + +/// Injects a content security policy to the HTML. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn inject_csp(document: &Document, csp: &str) { + let head = ensure_head(document); + let meta_tag = document.tree.new_element("meta"); + meta_tag.set_attr("http-equiv", "Content-Security-Policy"); + meta_tag.set_attr("content", csp); + head.append_child(&meta_tag); +} + +/// Injects a content security policy to the HTML. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn append_script_to_head(document: &Document, script: &str) { + let head = ensure_head(document); + let script_tag = document.tree.new_element("script"); + script_tag.set_text(script); + head.prepend_child(&script_tag); +} + +/// Injects the Isolation JavaScript to a codegen time document. +/// +/// Note: This function is not considered part of the stable API. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +#[cfg(feature = "isolation")] +pub fn inject_codegen_isolation_script(document: &Document) { + use crate::pattern::isolation::IsolationJavascriptCodegen; + use serialize_to_javascript::DefaultTemplate; + + let head = ensure_head(document); + + let script_content = IsolationJavascriptCodegen {} + .render_default(&Default::default()) + .expect("unable to render codegen isolation script template") + .into_string(); + + let script_tag = document.tree.new_element("script"); + script_tag.set_attr("nonce", SCRIPT_NONCE_TOKEN); + script_tag.set_text(script_content); + + head.prepend_child(&script_tag); +} + +/// Temporary workaround for Windows not allowing requests +/// +/// Note: this does not prevent path traversal due to the isolation application expectation that it +/// is secure. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +#[cfg(feature = "isolation")] +pub fn inline_isolation(document: &Document, dir: &std::path::Path) { + let scripts = document.select("script[src]"); + + for script in scripts.nodes() { + let src = match script.attr("src") { + Some(s) => s.to_string(), + None => continue, + }; + + let mut path = std::path::PathBuf::from(src); + if path.has_root() { + path = path + .strip_prefix("/") + .expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.") + .into(); + } + + let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file"); + + script.set_text(file); + script.remove_attr("src"); + } +} + +// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842 +/// Normalize line endings in script content to match what the browser uses for CSP hashing. +/// +/// According to the HTML spec, browsers normalize: +/// - `\r\n` → `\n` +/// - `\r` → `\n` +pub fn normalize_script_for_csp(input: &[u8]) -> Vec { + let mut output = Vec::with_capacity(input.len()); + + let mut i = 0; + while i < input.len() { + match input[i] { + b'\r' => { + if i + 1 < input.len() && input[i + 1] == b'\n' { + // CRLF → LF + output.push(b'\n'); + i += 2; + } else { + // Lone CR → LF + output.push(b'\n'); + i += 1; + } + } + _ => { + output.push(input[i]); + i += 1; + } + } + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config, + }; + + #[test] + fn csp() { + let htmls = vec![ + "".to_string(), + "".to_string(), + ]; + + for html in htmls { + let document = parse_doc(html); + let csp = "csp-string"; + inject_csp(&document, csp); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!( + r#""# + ) + ); + } + } + + #[test] + fn normalize_script_for_csp_test() { + let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n"; + let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n"; + + assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes()) + } + + #[test] + fn parse_and_serialize_roundtrips() { + let htmls = [ + "Test

Hello

", + "", + ]; + + for html in htmls { + let parsed = parse_doc(html.to_string()); + let serialized = serialize_doc(&parsed); + let result = String::from_utf8(serialized).unwrap(); + + assert_eq!(result, html); + } + } + + #[test] + fn inject_nonce_to_scripts() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn inject_nonce_to_styles() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn append_script_to_head_test() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + append_script_to_head(&document, r#"console.log('Test')"#); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!(r#""#) + ); + } + + #[test] + fn inject_nonce_skips_existing() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!(String::from_utf8(serialize_doc(&document)).unwrap(), html); + } + + #[test] + fn inject_nonce_respects_disabled_modification() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true)); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + r#""# + ); + } + + #[test] + #[cfg(feature = "isolation")] + fn inline_isolation_replaces_src_with_content() { + use std::io::Write; + + let temp_dir = tempfile::tempdir().unwrap(); + let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap(); + file.write_all(b"console.log('test');").unwrap(); + let file_name = file.path().file_name().unwrap().to_str().unwrap(); + + let html = + format!(r#""#); + let document = parse_doc(html); + inline_isolation(&document, temp_dir.path()); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + r#""# + ); + } +} diff --git a/crates/tauri-utils/src/lib.rs b/crates/tauri-utils/src/lib.rs index 5070ff7f35d0..fb85d7dcfd5e 100644 --- a/crates/tauri-utils/src/lib.rs +++ b/crates/tauri-utils/src/lib.rs @@ -26,6 +26,8 @@ pub mod config; pub mod config_v1; #[cfg(feature = "html-manipulation")] pub mod html; +#[cfg(feature = "html-manipulation-2")] +pub mod html2; pub mod io; pub mod mime_type; pub mod platform; @@ -33,10 +35,10 @@ pub mod plugin; /// Prepare application resources and sidecars. #[cfg(feature = "resources")] pub mod resources; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub mod tokens; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub mod build; /// Application pattern. diff --git a/crates/tauri-utils/src/platform.rs b/crates/tauri-utils/src/platform.rs index ebb3858824ba..708f9684f085 100644 --- a/crates/tauri-utils/src/platform.rs +++ b/crates/tauri-utils/src/platform.rs @@ -369,7 +369,7 @@ pub fn bundle_type() -> Option { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; diff --git a/crates/tauri-utils/src/platform/starting_binary.rs b/crates/tauri-utils/src/platform/starting_binary.rs index 05e28c7fb354..4e6d29c3cc2f 100644 --- a/crates/tauri-utils/src/platform/starting_binary.rs +++ b/crates/tauri-utils/src/platform/starting_binary.rs @@ -11,7 +11,7 @@ use std::{ /// A cached version of the current binary using [`ctor`] to cache it before even `main` runs. #[ctor] #[used] -pub(super) static STARTING_BINARY: StartingBinary = StartingBinary::new(); +pub(super) static STARTING_BINARY: StartingBinary = unsafe { StartingBinary::new() }; /// Represents a binary path that was cached when the program was loaded. pub(super) struct StartingBinary(std::io::Result); diff --git a/crates/tauri-utils/src/plugin.rs b/crates/tauri-utils/src/plugin.rs index 8f178ca4163a..9fe35be2dfc4 100644 --- a/crates/tauri-utils/src/plugin.rs +++ b/crates/tauri-utils/src/plugin.rs @@ -3,10 +3,10 @@ // SPDX-License-Identifier: MIT //! Compile-time and runtime types for Tauri plugins. -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub use build::*; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use std::{ env::vars_os, diff --git a/crates/tauri/CHANGELOG.md b/crates/tauri/CHANGELOG.md index 3572afd7ab04..0803ad1a511c 100644 --- a/crates/tauri/CHANGELOG.md +++ b/crates/tauri/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog +## \[2.11.1] + +### Enhancements + +- [`5e3126ff7`](https://www.github.com/tauri-apps/tauri/commit/5e3126ff7045aec54811b227cb4d33d78b3957b5) ([#15338](https://www.github.com/tauri-apps/tauri/pull/15338)) Expose the monitor (display) APIs on mobile. + +### Bug Fixes + +- [`5f479c0c3`](https://www.github.com/tauri-apps/tauri/commit/5f479c0c364d7f5d89a83eaff66fbb7ef5045ce9) ([#15336](https://www.github.com/tauri-apps/tauri/pull/15336)) Fix crash when using the requestPermission API on Android. + +### Security fixes + +- [`1b26769f9`](https://www.github.com/tauri-apps/tauri/commit/1b26769f92b54b158777a35a7f548f870f4e7901) ([#15266](https://www.github.com/tauri-apps/tauri/pull/15266)) Enforce ACL checks for IPC requests from remote origins even when no `AppManifest` is configured. Previously, custom (non-plugin) commands bypassed ACL entirely without an `AppManifest`, allowing any origin to invoke them. Now, remote origins are always subject to ACL resolution, and can only reach custom commands if an explicit `remote` capability has been granted. +- [`ba025588f`](https://www.github.com/tauri-apps/tauri/commit/ba025588f3559858f43547e8c04424c47a3c445b) Correctly handle .localhost suffix in local origins on Windows and Android to fix a security issue that made tauri think remote websites that started with a registered scheme were local websites. + For example, when registering an `app` custom protocol, Tauri would think `http://app.evil.com/` would be a local URL on Windows/Android. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-runtime@2.11.1` +- Upgraded to `tauri-runtime-wry@2.11.1` +- Upgraded to `tauri-macros@2.6.1` +- Upgraded to `tauri-build@2.6.1` + +## \[2.11.0] + +### New Features + +- [`074299c08`](https://www.github.com/tauri-apps/tauri/commit/074299c08dd99d2e1c57796f55ab24bc1d3976cc) ([#14307](https://www.github.com/tauri-apps/tauri/pull/14307)) Add Bring All to Front predefined menu item type +- [`c00a3dbff`](https://www.github.com/tauri-apps/tauri/commit/c00a3dbffccd6e051d3b7332f706b6c63759865d) ([#14473](https://www.github.com/tauri-apps/tauri/pull/14473)) Add support for the `rename` attribute in the `tauri::command` macro to allow renaming the command to something other than the function name. +- [`a12142a48`](https://www.github.com/tauri-apps/tauri/commit/a12142a481f7a19b69e88ee36a438b1db71b36f5) ([#14357](https://www.github.com/tauri-apps/tauri/pull/14357)) Add macos support for setting the icon and icon template state in the same step of the main thread, to prevent flickering. +- [`2dd9b15a2`](https://www.github.com/tauri-apps/tauri/commit/2dd9b15a2bcab8e52c87b03a919b4a75567ad3ce) ([#15062](https://www.github.com/tauri-apps/tauri/pull/15062)) Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"` +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`b27be063f`](https://www.github.com/tauri-apps/tauri/commit/b27be063ff3052cb1071ac3ec719cfa104460fa4) ([#14925](https://www.github.com/tauri-apps/tauri/pull/14925)) Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers. +- [`d83d2d92b`](https://www.github.com/tauri-apps/tauri/commit/d83d2d92b4327da3dbac60f83cada36c8ec194dc) ([#14905](https://www.github.com/tauri-apps/tauri/pull/14905)) Enable track_caller attribute for async_runtime to provide better location information in logs and panics. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Trigger `RunEvent::Opened` on Android. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Added `dbus` feature flag (enabled by default) which is required for theme detection on Linux. +- [`1063c48c5`](https://www.github.com/tauri-apps/tauri/commit/1063c48c5e7d099ad74d28a937edf42e3f5c9f03) ([#14523](https://www.github.com/tauri-apps/tauri/pull/14523)) Add handler for web content process termination on macOS and iOS. + +### Enhancements + +- [`d730770bb`](https://www.github.com/tauri-apps/tauri/commit/d730770bb93d77358cfc6f1286f10187cef37362) ([#15117](https://www.github.com/tauri-apps/tauri/pull/15117)) Simplify async-sync code boundaries, no externally visible changes +- [`c69d5ca4b`](https://www.github.com/tauri-apps/tauri/commit/c69d5ca4b1a646843c3f250a0d1b13414c5e8223) ([#15262](https://www.github.com/tauri-apps/tauri/pull/15262)) Remove a clone, no user-facing changes. +- [`4017a7ed7`](https://www.github.com/tauri-apps/tauri/commit/4017a7ed7313cebf912ef3af1e3b280855b6f100) ([#14908](https://www.github.com/tauri-apps/tauri/pull/14908)) Implement retrieving inner PathBuf from SafePathBuf to ease using APIs that require an owned PathBuf + +### Bug Fixes + +- [`110336c88`](https://www.github.com/tauri-apps/tauri/commit/110336c88a8c0a04476619db0a5c8f7694d969a5) ([#15250](https://www.github.com/tauri-apps/tauri/pull/15250)) Fix initial window position when positioning it to another monitor. +- [`9808236eb`](https://www.github.com/tauri-apps/tauri/commit/9808236ebf7755d498d674b614f3fc75eeac1ec4) ([#14655](https://www.github.com/tauri-apps/tauri/pull/14655)) Fix monitor work area Y position on macOS. + +### What's Changed + +- [`d34497ef1`](https://www.github.com/tauri-apps/tauri/commit/d34497ef154eddcc36327a30dda06dc4748f6b20) ([#14862](https://www.github.com/tauri-apps/tauri/pull/14862)) The new window handler passed to `on_new_window` no longer requires `Sync`, and runs on main thread on Windows, aligning with other platforms + +### Dependencies + +- Upgraded to `tauri-macros@2.6.0` +- Upgraded to `tauri-build@2.6.0` +- Upgraded to `tauri-runtime@2.11.0` +- Upgraded to `tauri-runtime-wry@2.11.0` +- Upgraded to `tauri-utils@2.9.0` +- [`373b7e677`](https://www.github.com/tauri-apps/tauri/commit/373b7e677ec498899759de9fcd35941fe792b58b) ([#15177](https://www.github.com/tauri-apps/tauri/pull/15177)) Update Specta in lockfile and upgrade dependencies using the removed `doc_auto_cfg` attribute to fix errors building documentation +- [`a219ede00`](https://www.github.com/tauri-apps/tauri/commit/a219ede0003bb8073d8002be42bcf343538c42f8) ([#15203](https://www.github.com/tauri-apps/tauri/pull/15203)) Updated `tray-icon` to v0.22 + ## \[2.10.3] ### Dependencies diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 3faf5c3be2cd..e812fe57e6e7 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri" -version = "2.10.3" +version = "2.11.1" description = "Make tiny, secure apps for all desktop platforms with Tauri" exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"] readme = "README.md" @@ -26,6 +26,7 @@ features = [ "test", "specta", "dynamic-acl", + "isolation", ] default-target = "x86_64-unknown-linux-gnu" targets = [ @@ -55,12 +56,12 @@ uuid = { version = "1", features = ["v4"], optional = true } url = "2" anyhow = "1" thiserror = "2" -tauri-runtime = { version = "2.10.1", path = "../tauri-runtime" } -tauri-macros = { version = "2.5.5", path = "../tauri-macros" } -tauri-utils = { version = "2.8.3", features = [ +tauri-runtime = { version = "2.11.1", path = "../tauri-runtime" } +tauri-macros = { version = "2.6.1", path = "../tauri-macros" } +tauri-utils = { version = "2.9.1", features = [ "resources", ], path = "../tauri-utils" } -tauri-runtime-wry = { version = "2.10.1", path = "../tauri-runtime-wry", default-features = false, optional = true } +tauri-runtime-wry = { version = "2.11.1", path = "../tauri-runtime-wry", default-features = false, optional = true } tauri-runtime-cef = { version = "0.1.0", path = "../tauri-runtime-cef", optional = true } getrandom = "0.3" serde_repr = "0.1" @@ -97,11 +98,11 @@ rustls = { version = "0.23", default-features = false, features = [ # desktop [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"))'.dependencies] -muda = { version = "0.17", default-features = false, features = [ +muda = { version = "0.19", default-features = false, features = [ "serde", "gtk", ] } -tray-icon = { version = "0.21", default-features = false, features = [ +tray-icon = { version = "0.23", default-features = false, features = [ "serde", ], optional = true } @@ -159,15 +160,17 @@ jni = "0.21" libc = "0.2" swift-rs = "1" objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ + "UIApplication", + "UIResponder", "UIView", ] } [build-dependencies] glob = "0.3" heck = "0.5" -tauri-build = { path = "../tauri-build/", default-features = false, version = "2.5.6" } -tauri-utils = { path = "../tauri-utils/", version = "2.8.3", features = [ - "build", +tauri-build = { path = "../tauri-build/", default-features = false, version = "2.6.1" } +tauri-utils = { path = "../tauri-utils/", version = "2.9.1", features = [ + "build-2", ] } [dev-dependencies] @@ -182,9 +185,17 @@ cargo_toml = "0.22" http-range = "0.1.5" [features] -default = ["wry", "compression", "common-controls-v6", "dynamic-acl", "x11"] +default = [ + "wry", + "compression", + "common-controls-v6", + "dynamic-acl", + "x11", + "dbus", +] unstable = ["tauri-runtime-wry?/unstable"] x11 = ["tauri-runtime-wry?/x11"] +dbus = ["tauri-runtime-wry?/dbus"] common-controls-v6 = [ "tray-icon?/common-controls-v6", "muda/common-controls-v6", @@ -218,7 +229,7 @@ macos-private-api = [ "tauri-runtime-wry?/macos-private-api", "tauri-runtime-cef?/macos-private-api", ] -webview-data-url = ["data-url", "tauri-utils/html-manipulation"] +webview-data-url = ["data-url", "tauri-utils/html-manipulation-2"] protocol-asset = ["http-range"] config-json5 = ["tauri-macros/config-json5"] config-toml = ["tauri-macros/config-toml"] diff --git a/crates/tauri/build.rs b/crates/tauri/build.rs index 0296aebe06c2..a1b9d078f6e9 100644 --- a/crates/tauri/build.rs +++ b/crates/tauri/build.rs @@ -69,6 +69,8 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[ ("cursor_position", true), ("theme", true), ("is_always_on_top", true), + ("activity_name", true), + ("scene_identifier", true), // setters ("center", false), ("request_user_attention", false), @@ -166,6 +168,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[ ("bundle_type", true), ("register_listener", true), ("remove_listener", true), + ("supports_multiple_windows", true), ], ), ( @@ -219,6 +222,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[ ("set_visible", true), ("set_temp_dir_path", true), ("set_icon_as_template", true), + ("set_icon_with_as_template", true), ("set_show_menu_on_left_click", true), ], ), diff --git a/crates/tauri/mobile/android-codegen/TauriActivity.kt b/crates/tauri/mobile/android-codegen/TauriActivity.kt index 1c96394b89a7..251c41e3598e 100644 --- a/crates/tauri/mobile/android-codegen/TauriActivity.kt +++ b/crates/tauri/mobile/android-codegen/TauriActivity.kt @@ -8,44 +8,58 @@ package {{package}} import android.content.Intent import android.content.res.Configuration +import android.os.Bundle import app.tauri.plugin.PluginManager +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner + +object TauriLifecycleObserver : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + PluginManager.onResume() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + PluginManager.onPause() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + PluginManager.onStop() + } +} abstract class TauriActivity : WryActivity() { - var pluginManager: PluginManager = PluginManager(this) override val handleBackNavigation: Boolean = false - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - pluginManager.onNewIntent(intent) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + PluginManager.onActivityCreate(this) } - override fun onResume() { - super.onResume() - pluginManager.onResume() + fun getPluginManager(): PluginManager { + return PluginManager } - override fun onPause() { - super.onPause() - pluginManager.onPause() + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + PluginManager.onNewIntent(intent) } override fun onRestart() { super.onRestart() - pluginManager.onRestart() - } - - override fun onStop() { - super.onStop() - pluginManager.onStop() + PluginManager.onRestart(this) } override fun onDestroy() { super.onDestroy() - pluginManager.onDestroy() + PluginManager.onDestroy(this) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - pluginManager.onConfigurationChanged(newConfig) + PluginManager.onConfigurationChanged(newConfig) } } diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt index d33fa8a4d15a..fda20dd03f07 100644 --- a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt @@ -11,6 +11,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.webkit.WebView import androidx.activity.result.IntentSenderRequest +import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import app.tauri.FsUtils import app.tauri.Logger @@ -71,10 +72,18 @@ abstract class Plugin(private val activity: Activity) { */ open fun onResume() {} + + /** + * This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it). + * It will be followed by onStart() and then onResume(). + */ + open fun onRestart(activity: AppCompatActivity) {} + /** * This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it). * It will be followed by onStart() and then onResume(). */ + @Deprecated("use onRestart(activity: AppCompatActivity) instead") open fun onRestart() {} /** @@ -86,8 +95,23 @@ abstract class Plugin(private val activity: Activity) { /** * This event is called before the activity is destroyed. */ + open fun onDestroy(activity: AppCompatActivity) {} + /** + * This event is called before an activity is destroyed. + */ + @Deprecated("use onDestroy(activity: AppCompatActivity) instead") open fun onDestroy() {} + internal fun triggerOnDestroy(activity: AppCompatActivity) { + onDestroy(activity) + onDestroy() + } + + internal fun triggerOnRestart(activity: AppCompatActivity) { + onRestart(activity) + onRestart() + } + /** * This event is called when a configuration change occurs but the app does not recreate the activity. */ diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt index 362896b706d0..0c64bb5f3411 100644 --- a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt @@ -4,7 +4,6 @@ package app.tauri.plugin -import android.app.PendingIntent import android.content.res.Configuration import android.content.Context import android.content.Intent @@ -26,7 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import java.lang.reflect.InvocationTargetException -class PluginManager(val activity: AppCompatActivity) { +object PluginManager { fun interface RequestPermissionsCallback { fun onResult(permissions: Map) } @@ -35,16 +34,33 @@ class PluginManager(val activity: AppCompatActivity) { fun onResult(result: ActivityResult) } + lateinit var activity: AppCompatActivity private val plugins: HashMap = HashMap() - private val startActivityForResultLauncher: ActivityResultLauncher - private val startIntentSenderForResultLauncher: ActivityResultLauncher - private val requestPermissionsLauncher: ActivityResultLauncher> + private lateinit var startActivityForResultLauncher: ActivityResultLauncher + private lateinit var startIntentSenderForResultLauncher: ActivityResultLauncher + private lateinit var requestPermissionsLauncher: ActivityResultLauncher> private var requestPermissionsCallback: RequestPermissionsCallback? = null private var startActivityForResultCallback: ActivityResultCallback? = null private var startIntentSenderForResultCallback: ActivityResultCallback? = null - private var jsonMapper: ObjectMapper + private var jsonMapper: ObjectMapper = ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) init { + val channelDeserializer = ChannelDeserializer({ channelId, payload -> + sendChannelData(channelId, payload) + }, jsonMapper) + jsonMapper + .registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer)) + } + + fun onActivityCreate(activity: AppCompatActivity) { + // TODO: on destroy, we should change to a different activity + if (::activity.isInitialized) { + return + } + this.activity = activity startActivityForResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult() ) { result -> @@ -68,17 +84,6 @@ class PluginManager(val activity: AppCompatActivity) { requestPermissionsCallback!!.onResult(result) } } - - jsonMapper = ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) - .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - - val channelDeserializer = ChannelDeserializer({ channelId, payload -> - sendChannelData(channelId, payload) - }, jsonMapper) - jsonMapper - .registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer)) } fun onNewIntent(intent: Intent) { @@ -99,9 +104,9 @@ class PluginManager(val activity: AppCompatActivity) { } } - fun onRestart() { + fun onRestart(activity: AppCompatActivity) { for (plugin in plugins.values) { - plugin.instance.onRestart() + plugin.instance.triggerOnRestart(activity) } } @@ -111,9 +116,9 @@ class PluginManager(val activity: AppCompatActivity) { } } - fun onDestroy() { + fun onDestroy(activity: AppCompatActivity) { for (plugin in plugins.values) { - plugin.instance.onDestroy() + plugin.instance.triggerOnDestroy(activity) } } @@ -201,14 +206,12 @@ class PluginManager(val activity: AppCompatActivity) { } } - companion object { - fun loadConfig(context: Context, plugin: String, cls: Class): T { - val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json") - val mapper = ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - val config = mapper.readValue(tauriConfigJson, Config::class.java) - return mapper.readValue(config.plugins[plugin].toString(), cls) - } + fun loadConfig(context: Context, plugin: String, cls: Class): T { + val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json") + val mapper = ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val config = mapper.readValue(tauriConfigJson, Config::class.java) + return mapper.readValue(config.plugins[plugin].toString(), cls) } private external fun handlePluginResponse(id: Int, success: String?, error: String?) diff --git a/crates/tauri/permissions/app/autogenerated/reference.md b/crates/tauri/permissions/app/autogenerated/reference.md index 178a632d93ab..581bd8da85e8 100644 --- a/crates/tauri/permissions/app/autogenerated/reference.md +++ b/crates/tauri/permissions/app/autogenerated/reference.md @@ -11,6 +11,7 @@ Default permissions for the plugin. - `allow-bundle-type` - `allow-register-listener` - `allow-remove-listener` +- `allow-supports-multiple-windows` ## Permission Table @@ -336,6 +337,32 @@ Denies the set_dock_visibility command without any pre-configured scope. +`core:app:allow-supports-multiple-windows` + + + + +Enables the supports_multiple_windows command without any pre-configured scope. + + + + + + + +`core:app:deny-supports-multiple-windows` + + + + +Denies the supports_multiple_windows command without any pre-configured scope. + + + + + + + `core:app:allow-tauri-version` diff --git a/crates/tauri/permissions/tray/autogenerated/reference.md b/crates/tauri/permissions/tray/autogenerated/reference.md index 6ea38e1c9af4..341858892145 100644 --- a/crates/tauri/permissions/tray/autogenerated/reference.md +++ b/crates/tauri/permissions/tray/autogenerated/reference.md @@ -14,6 +14,7 @@ Default permissions for the plugin, which enables all commands. - `allow-set-visible` - `allow-set-temp-dir-path` - `allow-set-icon-as-template` +- `allow-set-icon-with-as-template` - `allow-set-show-menu-on-left-click` ## Permission Table @@ -158,6 +159,32 @@ Denies the set_icon_as_template command without any pre-configured scope. +`core:tray:allow-set-icon-with-as-template` + + + + +Enables the set_icon_with_as_template command without any pre-configured scope. + + + + + + + +`core:tray:deny-set-icon-with-as-template` + + + + +Denies the set_icon_with_as_template command without any pre-configured scope. + + + + + + + `core:tray:allow-set-menu` diff --git a/crates/tauri/permissions/window/autogenerated/reference.md b/crates/tauri/permissions/window/autogenerated/reference.md index 00158661d0ed..9d591ba38e3d 100644 --- a/crates/tauri/permissions/window/autogenerated/reference.md +++ b/crates/tauri/permissions/window/autogenerated/reference.md @@ -29,6 +29,8 @@ Default permissions for the plugin. - `allow-cursor-position` - `allow-theme` - `allow-is-always-on-top` +- `allow-activity-name` +- `allow-scene-identifier` - `allow-internal-toggle-maximize` ## Permission Table @@ -40,6 +42,32 @@ Default permissions for the plugin. + + + +`core:window:allow-activity-name` + + + + +Enables the activity_name command without any pre-configured scope. + + + + + + + +`core:window:deny-activity-name` + + + + +Denies the activity_name command without any pre-configured scope. + + + + @@ -875,6 +903,32 @@ Denies the scale_factor command without any pre-configured scope. +`core:window:allow-scene-identifier` + + + + +Enables the scene_identifier command without any pre-configured scope. + + + + + + + +`core:window:deny-scene-identifier` + + + + +Denies the scene_identifier command without any pre-configured scope. + + + + + + + `core:window:allow-set-always-on-bottom` diff --git a/crates/tauri/scripts/bundle.global.js b/crates/tauri/scripts/bundle.global.js index 7f8eff34b263..0441f23e6912 100644 --- a/crates/tauri/scripts/bundle.global.js +++ b/crates/tauri/scripts/bundle.global.js @@ -1 +1 @@ -var __TAURI_IIFE__=function(e){"use strict";function n(e,n,t,i){if("a"===t&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof n?e!==n||!i:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?i:"a"===t?i.call(e):i?i.value:n.get(e)}function t(e,n,t,i,r){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?r.call(e,t):r?r.value=t:n.set(e,t),t}var i,r,s,a,l;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";function u(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}class c{constructor(e){i.set(this,void 0),r.set(this,0),s.set(this,[]),a.set(this,void 0),t(this,i,e||(()=>{}),"f"),this.id=u(e=>{const l=e.index;if("end"in e)return void(l==n(this,r,"f")?this.cleanupCallback():t(this,a,l,"f"));const o=e.message;if(l==n(this,r,"f")){for(n(this,i,"f").call(this,o),t(this,r,n(this,r,"f")+1,"f");n(this,r,"f")in n(this,s,"f");){const e=n(this,s,"f")[n(this,r,"f")];n(this,i,"f").call(this,e),delete n(this,s,"f")[n(this,r,"f")],t(this,r,n(this,r,"f")+1,"f")}n(this,r,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,s,"f")[l]=o})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){t(this,i,e,"f")}get onmessage(){return n(this,i,"f")}[(i=new WeakMap,r=new WeakMap,s=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}class d{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return h(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function p(e,n,t){const i=new c(t);try{return await h(`plugin:${e}|register_listener`,{event:n,handler:i}),new d(e,n,i.id)}catch{return await h(`plugin:${e}|registerListener`,{event:n,handler:i}),new d(e,n,i.id)}}async function h(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}class w{get rid(){return n(this,l,"f")}constructor(e){l.set(this,void 0),t(this,l,e,"f")}async close(){return h("plugin:resources|close",{rid:this.rid})}}l=new WeakMap;var _=Object.freeze({__proto__:null,Channel:c,PluginListener:d,Resource:w,SERIALIZE_TO_IPC_FN:o,addPluginListener:p,checkPermissions:async function(e){return h(`plugin:${e}|check_permissions`)},convertFileSrc:function(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)},invoke:h,isTauri:function(){return!!(globalThis||window).isTauri},requestPermissions:async function(e){return h(`plugin:${e}|request_permissions`)},transformCallback:u});class y extends w{constructor(e){super(e)}static async new(e,n,t){return h("plugin:image|new",{rgba:g(e),width:n,height:t}).then(e=>new y(e))}static async fromBytes(e){return h("plugin:image|from_bytes",{bytes:g(e)}).then(e=>new y(e))}static async fromPath(e){return h("plugin:image|from_path",{path:e}).then(e=>new y(e))}async rgba(){return h("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return h("plugin:image|size",{rid:this.rid})}}function g(e){return null==e?null:"string"==typeof e?e:e instanceof y?e.rid:e}var b,m=Object.freeze({__proto__:null,Image:y,transformImage:g});!function(e){e.Nsis="nsis",e.Msi="msi",e.Deb="deb",e.Rpm="rpm",e.AppImage="appimage",e.App="app"}(b||(b={}));var f=Object.freeze({__proto__:null,get BundleType(){return b},defaultWindowIcon:async function(){return h("plugin:app|default_window_icon").then(e=>e?new y(e):null)},fetchDataStoreIdentifiers:async function(){return h("plugin:app|fetch_data_store_identifiers")},getBundleType:async function(){return h("plugin:app|bundle_type")},getIdentifier:async function(){return h("plugin:app|identifier")},getName:async function(){return h("plugin:app|name")},getTauriVersion:async function(){return h("plugin:app|tauri_version")},getVersion:async function(){return h("plugin:app|version")},hide:async function(){return h("plugin:app|app_hide")},onBackButtonPress:async function(e){return p("app","back-button",e)},removeDataStore:async function(e){return h("plugin:app|remove_data_store",{uuid:e})},setDockVisibility:async function(e){return h("plugin:app|set_dock_visibility",{visible:e})},setTheme:async function(e){return h("plugin:app|set_app_theme",{theme:e})},show:async function(){return h("plugin:app|app_show")}});class v{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.width=e[0].Logical.width,this.height=e[0].Logical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toPhysical(e){return new k(this.width*e,this.height*e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class k{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.width=e[0].Physical.width,this.height=e[0].Physical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toLogical(e){return new v(this.width/e,this.height/e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class A{constructor(e){this.size=e}toLogical(e){return this.size instanceof v?this.size:this.size.toLogical(e)}toPhysical(e){return this.size instanceof k?this.size:this.size.toPhysical(e)}[o](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[o]()}}class T{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.x=e[0].Logical.x,this.y=e[0].Logical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toPhysical(e){return new I(this.x*e,this.y*e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class I{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.x=e[0].Physical.x,this.y=e[0].Physical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toLogical(e){return new T(this.x/e,this.y/e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class E{constructor(e){this.position=e}toLogical(e){return this.position instanceof T?this.position:this.position.toLogical(e)}toPhysical(e){return this.position instanceof I?this.position:this.position.toPhysical(e)}[o](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[o]()}}var R,D=Object.freeze({__proto__:null,LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,Position:E,Size:A});async function S(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await h("plugin:event|unlisten",{event:e,eventId:n})}async function N(e,n,t){var i;const r="string"==typeof(null==t?void 0:t.target)?{kind:"AnyLabel",label:t.target}:null!==(i=null==t?void 0:t.target)&&void 0!==i?i:{kind:"Any"};return h("plugin:event|listen",{event:e,target:r,handler:u(n)}).then(n=>async()=>S(e,n))}async function L(e,n,t){return N(e,t=>{S(e,t.id),n(t)},t)}async function C(e,n){await h("plugin:event|emit",{event:e,payload:n})}async function x(e,n,t){const i="string"==typeof e?{kind:"AnyLabel",label:e}:e;await h("plugin:event|emit_to",{target:i,event:n,payload:t})}!function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(R||(R={}));var P,z,W,O=Object.freeze({__proto__:null,get TauriEvent(){return R},emit:C,emitTo:x,listen:N,once:L});function U(e){var n;if("items"in e)e.items=null===(n=e.items)||void 0===n?void 0:n.map(e=>"rid"in e?e:U(e));else if("action"in e&&e.action){const n=new c;return n.onmessage=e.action,delete e.action,{...e,handler:n}}return e}async function F(e,n){const t=new c;if(n&&"object"==typeof n&&("action"in n&&n.action&&(t.onmessage=n.action,delete n.action),"item"in n&&n.item&&"object"==typeof n.item&&"About"in n.item&&n.item.About&&"object"==typeof n.item.About&&"icon"in n.item.About&&n.item.About.icon&&(n.item.About.icon=g(n.item.About.icon)),"icon"in n&&n.icon&&(n.icon=g(n.icon)),"items"in n&&n.items)){function i(e){var n;return"rid"in e?[e.rid,e.kind]:("item"in e&&"object"==typeof e.item&&(null===(n=e.item.About)||void 0===n?void 0:n.icon)&&(e.item.About.icon=g(e.item.About.icon)),"icon"in e&&e.icon&&(e.icon=g(e.icon)),"items"in e&&e.items&&(e.items=e.items.map(i)),U(e))}n.items=n.items.map(i)}return h("plugin:menu|new",{kind:e,options:n,handler:t})}class M extends w{get id(){return n(this,P,"f")}get kind(){return n(this,z,"f")}constructor(e,n,i){super(e),P.set(this,void 0),z.set(this,void 0),t(this,P,n,"f"),t(this,z,i,"f")}}P=new WeakMap,z=new WeakMap;class B extends M{constructor(e,n){super(e,n,"MenuItem")}static async new(e){return F("MenuItem",e).then(([e,n])=>new B(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}}class V extends M{constructor(e,n){super(e,n,"Check")}static async new(e){return F("Check",e).then(([e,n])=>new V(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async isChecked(){return h("plugin:menu|is_checked",{rid:this.rid})}async setChecked(e){return h("plugin:menu|set_checked",{rid:this.rid,checked:e})}}!function(e){e.Add="Add",e.Advanced="Advanced",e.Bluetooth="Bluetooth",e.Bookmarks="Bookmarks",e.Caution="Caution",e.ColorPanel="ColorPanel",e.ColumnView="ColumnView",e.Computer="Computer",e.EnterFullScreen="EnterFullScreen",e.Everyone="Everyone",e.ExitFullScreen="ExitFullScreen",e.FlowView="FlowView",e.Folder="Folder",e.FolderBurnable="FolderBurnable",e.FolderSmart="FolderSmart",e.FollowLinkFreestanding="FollowLinkFreestanding",e.FontPanel="FontPanel",e.GoLeft="GoLeft",e.GoRight="GoRight",e.Home="Home",e.IChatTheater="IChatTheater",e.IconView="IconView",e.Info="Info",e.InvalidDataFreestanding="InvalidDataFreestanding",e.LeftFacingTriangle="LeftFacingTriangle",e.ListView="ListView",e.LockLocked="LockLocked",e.LockUnlocked="LockUnlocked",e.MenuMixedState="MenuMixedState",e.MenuOnState="MenuOnState",e.MobileMe="MobileMe",e.MultipleDocuments="MultipleDocuments",e.Network="Network",e.Path="Path",e.PreferencesGeneral="PreferencesGeneral",e.QuickLook="QuickLook",e.RefreshFreestanding="RefreshFreestanding",e.Refresh="Refresh",e.Remove="Remove",e.RevealFreestanding="RevealFreestanding",e.RightFacingTriangle="RightFacingTriangle",e.Share="Share",e.Slideshow="Slideshow",e.SmartBadge="SmartBadge",e.StatusAvailable="StatusAvailable",e.StatusNone="StatusNone",e.StatusPartiallyAvailable="StatusPartiallyAvailable",e.StatusUnavailable="StatusUnavailable",e.StopProgressFreestanding="StopProgressFreestanding",e.StopProgress="StopProgress",e.TrashEmpty="TrashEmpty",e.TrashFull="TrashFull",e.User="User",e.UserAccounts="UserAccounts",e.UserGroup="UserGroup",e.UserGuest="UserGuest"}(W||(W={}));class G extends M{constructor(e,n){super(e,n,"Icon")}static async new(e){return F("Icon",e).then(([e,n])=>new G(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class j extends M{constructor(e,n){super(e,n,"Predefined")}static async new(e){return F("Predefined",e).then(([e,n])=>new j(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}}function H([e,n,t]){switch(t){case"Submenu":return new $(e,n);case"Predefined":return new j(e,n);case"Check":return new V(e,n);case"Icon":return new G(e,n);default:return new B(e,n)}}class $ extends M{constructor(e,n){super(e,n,"Submenu")}static async new(e){return F("Submenu",e).then(([e,n])=>new $(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsWindowsMenuForNSApp(){return h("plugin:menu|set_as_windows_menu_for_nsapp",{rid:this.rid})}async setAsHelpMenuForNSApp(){return h("plugin:menu|set_as_help_menu_for_nsapp",{rid:this.rid})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class q extends M{constructor(e,n){super(e,n,"Menu")}static async new(e){return F("Menu",e).then(([e,n])=>new q(e,n))}static async default(){return h("plugin:menu|create_default").then(([e,n])=>new q(e,n))}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsAppMenu(){return h("plugin:menu|set_as_app_menu",{rid:this.rid}).then(e=>e?new q(e[0],e[1]):null)}async setAsWindowMenu(e){var n;return h("plugin:menu|set_as_window_menu",{rid:this.rid,window:null!==(n=null==e?void 0:e.label)&&void 0!==n?n:null}).then(e=>e?new q(e[0],e[1]):null)}}var J=Object.freeze({__proto__:null,CheckMenuItem:V,IconMenuItem:G,Menu:q,MenuItem:B,get NativeIcon(){return W},PredefinedMenuItem:j,Submenu:$,itemFromKind:H});function Q(){var e,n;window.__TAURI_INTERNALS__=null!==(e=window.__TAURI_INTERNALS__)&&void 0!==e?e:{},window.__TAURI_EVENT_PLUGIN_INTERNALS__=null!==(n=window.__TAURI_EVENT_PLUGIN_INTERNALS__)&&void 0!==n?n:{}}var Z,K=Object.freeze({__proto__:null,clearMocks:function(){"object"==typeof window.__TAURI_INTERNALS__&&(delete window.__TAURI_INTERNALS__.invoke,delete window.__TAURI_INTERNALS__.transformCallback,delete window.__TAURI_INTERNALS__.unregisterCallback,delete window.__TAURI_INTERNALS__.runCallback,delete window.__TAURI_INTERNALS__.callbacks,delete window.__TAURI_INTERNALS__.convertFileSrc,delete window.__TAURI_INTERNALS__.metadata,"object"==typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__&&delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener)},mockConvertFileSrc:function(e){Q(),window.__TAURI_INTERNALS__.convertFileSrc=function(n,t="asset"){const i=encodeURIComponent(n);return"windows"===e?`http://${t}.localhost/${i}`:`${t}://localhost/${i}`}},mockIPC:function(e,n){function t(e,n){switch(e){case"plugin:event|listen":return function(e){i.has(e.event)||i.set(e.event,[]);return i.get(e.event).push(e.handler),e.handler}(n);case"plugin:event|emit":return function(e){const n=i.get(e.event)||[];for(const t of n)a(t,e);return null}(n);case"plugin:event|unlisten":return function(e){const n=i.get(e.event);if(n){const t=n.indexOf(e.id);-1!==t&&n.splice(t,1)}}(n)}}Q();const i=new Map,r=new Map;function s(e){r.delete(e)}function a(e,n){const t=r.get(e);t?t(n):console.warn(`[TAURI] Couldn't find callback id ${e}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)}window.__TAURI_INTERNALS__.invoke=async function(i,r,s){return(null==n?void 0:n.shouldMockEvents)&&function(e){return e.startsWith("plugin:event|")}(i)?t(i,r):e(i,r)},window.__TAURI_INTERNALS__.transformCallback=function(e,n=!1){const t=window.crypto.getRandomValues(new Uint32Array(1))[0];return r.set(t,i=>(n&&s(t),e&&e(i))),t},window.__TAURI_INTERNALS__.unregisterCallback=s,window.__TAURI_INTERNALS__.runCallback=a,window.__TAURI_INTERNALS__.callbacks=r,window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener=function(e,n){s(n)}},mockWindows:function(e,...n){Q(),window.__TAURI_INTERNALS__.metadata={currentWindow:{label:e},currentWebview:{windowLabel:e,label:e}}}});!function(e){e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Document=6]="Document",e[e.Download=7]="Download",e[e.Picture=8]="Picture",e[e.Public=9]="Public",e[e.Video=10]="Video",e[e.Resource=11]="Resource",e[e.Temp=12]="Temp",e[e.AppConfig=13]="AppConfig",e[e.AppData=14]="AppData",e[e.AppLocalData=15]="AppLocalData",e[e.AppCache=16]="AppCache",e[e.AppLog=17]="AppLog",e[e.Desktop=18]="Desktop",e[e.Executable=19]="Executable",e[e.Font=20]="Font",e[e.Home=21]="Home",e[e.Runtime=22]="Runtime",e[e.Template=23]="Template"}(Z||(Z={}));var Y=Object.freeze({__proto__:null,get BaseDirectory(){return Z},appCacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppCache})},appConfigDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppConfig})},appDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppData})},appLocalDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLocalData})},appLogDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLog})},audioDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Audio})},basename:async function(e,n){return h("plugin:path|basename",{path:e,ext:n})},cacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Cache})},configDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Config})},dataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Data})},delimiter:function(){return window.__TAURI_INTERNALS__.plugins.path.delimiter},desktopDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Desktop})},dirname:async function(e){return h("plugin:path|dirname",{path:e})},documentDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Document})},downloadDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Download})},executableDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Executable})},extname:async function(e){return h("plugin:path|extname",{path:e})},fontDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Font})},homeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Home})},isAbsolute:async function(e){return h("plugin:path|is_absolute",{path:e})},join:async function(...e){return h("plugin:path|join",{paths:e})},localDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.LocalData})},normalize:async function(e){return h("plugin:path|normalize",{path:e})},pictureDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Picture})},publicDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Public})},resolve:async function(...e){return h("plugin:path|resolve",{paths:e})},resolveResource:async function(e){return h("plugin:path|resolve_directory",{directory:Z.Resource,path:e})},resourceDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Resource})},runtimeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Runtime})},sep:function(){return window.__TAURI_INTERNALS__.plugins.path.sep},tempDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Temp})},templateDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Template})},videoDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Video})}});class X extends w{constructor(e,n){super(e),this.id=n}static async getById(e){return h("plugin:tray|get_by_id",{id:e}).then(n=>n?new X(n,e):null)}static async removeById(e){return h("plugin:tray|remove_by_id",{id:e})}static async new(e){(null==e?void 0:e.menu)&&(e.menu=[e.menu.rid,e.menu.kind]),(null==e?void 0:e.icon)&&(e.icon=g(e.icon));const n=new c;if(null==e?void 0:e.action){const t=e.action;n.onmessage=e=>t(function(e){const n=e;return n.position=new I(e.position),n.rect.position=new I(e.rect.position),n.rect.size=new k(e.rect.size),n}(e)),delete e.action}return h("plugin:tray|new",{options:null!=e?e:{},handler:n}).then(([e,n])=>new X(e,n))}async setIcon(e){let n=null;return e&&(n=g(e)),h("plugin:tray|set_icon",{rid:this.rid,icon:n})}async setMenu(e){return e&&(e=[e.rid,e.kind]),h("plugin:tray|set_menu",{rid:this.rid,menu:e})}async setTooltip(e){return h("plugin:tray|set_tooltip",{rid:this.rid,tooltip:e})}async setTitle(e){return h("plugin:tray|set_title",{rid:this.rid,title:e})}async setVisible(e){return h("plugin:tray|set_visible",{rid:this.rid,visible:e})}async setTempDirPath(e){return h("plugin:tray|set_temp_dir_path",{rid:this.rid,path:e})}async setIconAsTemplate(e){return h("plugin:tray|set_icon_as_template",{rid:this.rid,asTemplate:e})}async setMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}async setShowMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}}var ee,ne,te=Object.freeze({__proto__:null,TrayIcon:X});!function(e){e[e.Critical=1]="Critical",e[e.Informational=2]="Informational"}(ee||(ee={}));class ie{constructor(e){this._preventDefault=!1,this.event=e.event,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}function re(){return new le(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function se(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new le(e,{skip:!0})))}!function(e){e.None="none",e.Normal="normal",e.Indeterminate="indeterminate",e.Paused="paused",e.Error="error"}(ne||(ne={}));const ae=["tauri://created","tauri://error"];class le{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:window|create",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await se()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return re()}static async getAll(){return se()}static async getFocusedWindow(){for(const e of await se())if(await e.isFocused())return e;return null}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Window",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Window",label:this.label}})}async emit(e,n){if(!ae.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ae.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ae.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async scaleFactor(){return h("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return h("plugin:window|inner_position",{label:this.label}).then(e=>new I(e))}async outerPosition(){return h("plugin:window|outer_position",{label:this.label}).then(e=>new I(e))}async innerSize(){return h("plugin:window|inner_size",{label:this.label}).then(e=>new k(e))}async outerSize(){return h("plugin:window|outer_size",{label:this.label}).then(e=>new k(e))}async isFullscreen(){return h("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return h("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return h("plugin:window|is_maximized",{label:this.label})}async isFocused(){return h("plugin:window|is_focused",{label:this.label})}async isDecorated(){return h("plugin:window|is_decorated",{label:this.label})}async isResizable(){return h("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return h("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return h("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return h("plugin:window|is_closable",{label:this.label})}async isVisible(){return h("plugin:window|is_visible",{label:this.label})}async title(){return h("plugin:window|title",{label:this.label})}async theme(){return h("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return h("plugin:window|is_always_on_top",{label:this.label})}async center(){return h("plugin:window|center",{label:this.label})}async requestUserAttention(e){let n=null;return e&&(n=e===ee.Critical?{type:"Critical"}:{type:"Informational"}),h("plugin:window|request_user_attention",{label:this.label,value:n})}async setResizable(e){return h("plugin:window|set_resizable",{label:this.label,value:e})}async setEnabled(e){return h("plugin:window|set_enabled",{label:this.label,value:e})}async isEnabled(){return h("plugin:window|is_enabled",{label:this.label})}async setMaximizable(e){return h("plugin:window|set_maximizable",{label:this.label,value:e})}async setMinimizable(e){return h("plugin:window|set_minimizable",{label:this.label,value:e})}async setClosable(e){return h("plugin:window|set_closable",{label:this.label,value:e})}async setTitle(e){return h("plugin:window|set_title",{label:this.label,value:e})}async maximize(){return h("plugin:window|maximize",{label:this.label})}async unmaximize(){return h("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return h("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return h("plugin:window|minimize",{label:this.label})}async unminimize(){return h("plugin:window|unminimize",{label:this.label})}async show(){return h("plugin:window|show",{label:this.label})}async hide(){return h("plugin:window|hide",{label:this.label})}async close(){return h("plugin:window|close",{label:this.label})}async destroy(){return h("plugin:window|destroy",{label:this.label})}async setDecorations(e){return h("plugin:window|set_decorations",{label:this.label,value:e})}async setShadow(e){return h("plugin:window|set_shadow",{label:this.label,value:e})}async setEffects(e){return h("plugin:window|set_effects",{label:this.label,value:e})}async clearEffects(){return h("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(e){return h("plugin:window|set_always_on_top",{label:this.label,value:e})}async setAlwaysOnBottom(e){return h("plugin:window|set_always_on_bottom",{label:this.label,value:e})}async setContentProtected(e){return h("plugin:window|set_content_protected",{label:this.label,value:e})}async setSize(e){return h("plugin:window|set_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setMinSize(e){return h("plugin:window|set_min_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setMaxSize(e){return h("plugin:window|set_max_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setSizeConstraints(e){function n(e){return e?{Logical:e}:null}return h("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:n(null==e?void 0:e.minWidth),minHeight:n(null==e?void 0:e.minHeight),maxWidth:n(null==e?void 0:e.maxWidth),maxHeight:n(null==e?void 0:e.maxHeight)}})}async setPosition(e){return h("plugin:window|set_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFullscreen(e){return h("plugin:window|set_fullscreen",{label:this.label,value:e})}async setSimpleFullscreen(e){return h("plugin:window|set_simple_fullscreen",{label:this.label,value:e})}async setFocus(){return h("plugin:window|set_focus",{label:this.label})}async setFocusable(e){return h("plugin:window|set_focusable",{label:this.label,value:e})}async setIcon(e){return h("plugin:window|set_icon",{label:this.label,value:g(e)})}async setSkipTaskbar(e){return h("plugin:window|set_skip_taskbar",{label:this.label,value:e})}async setCursorGrab(e){return h("plugin:window|set_cursor_grab",{label:this.label,value:e})}async setCursorVisible(e){return h("plugin:window|set_cursor_visible",{label:this.label,value:e})}async setCursorIcon(e){return h("plugin:window|set_cursor_icon",{label:this.label,value:e})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e})}async setCursorPosition(e){return h("plugin:window|set_cursor_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setIgnoreCursorEvents(e){return h("plugin:window|set_ignore_cursor_events",{label:this.label,value:e})}async startDragging(){return h("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(e){return h("plugin:window|start_resize_dragging",{label:this.label,value:e})}async setBadgeCount(e){return h("plugin:window|set_badge_count",{label:this.label,value:e})}async setBadgeLabel(e){return h("plugin:window|set_badge_label",{label:this.label,value:e})}async setOverlayIcon(e){return h("plugin:window|set_overlay_icon",{label:this.label,value:e?g(e):void 0})}async setProgressBar(e){return h("plugin:window|set_progress_bar",{label:this.label,value:e})}async setVisibleOnAllWorkspaces(e){return h("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:e})}async setTitleBarStyle(e){return h("plugin:window|set_title_bar_style",{label:this.label,value:e})}async setTheme(e){return h("plugin:window|set_theme",{label:this.label,value:e})}async onResized(e){return this.listen(R.WINDOW_RESIZED,n=>{n.payload=new k(n.payload),e(n)})}async onMoved(e){return this.listen(R.WINDOW_MOVED,n=>{n.payload=new I(n.payload),e(n)})}async onCloseRequested(e){return this.listen(R.WINDOW_CLOSE_REQUESTED,async n=>{const t=new ie(n);await e(t),t.isPreventDefault()||await this.destroy()})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}async onFocusChanged(e){const n=await this.listen(R.WINDOW_FOCUS,n=>{e({...n,payload:!0})}),t=await this.listen(R.WINDOW_BLUR,n=>{e({...n,payload:!1})});return()=>{n(),t()}}async onScaleChanged(e){return this.listen(R.WINDOW_SCALE_FACTOR_CHANGED,e)}async onThemeChanged(e){return this.listen(R.WINDOW_THEME_CHANGED,e)}}var oe,ue,ce,de;function pe(e){return null===e?null:{name:e.name,scaleFactor:e.scaleFactor,position:new I(e.position),size:new k(e.size),workArea:{position:new I(e.workArea.position),size:new k(e.workArea.size)}}}!function(e){e.Disabled="disabled",e.Throttle="throttle",e.Suspend="suspend"}(oe||(oe={})),function(e){e.Default="default",e.FluentOverlay="fluentOverlay"}(ue||(ue={})),function(e){e.AppearanceBased="appearanceBased",e.Light="light",e.Dark="dark",e.MediumLight="mediumLight",e.UltraDark="ultraDark",e.Titlebar="titlebar",e.Selection="selection",e.Menu="menu",e.Popover="popover",e.Sidebar="sidebar",e.HeaderView="headerView",e.Sheet="sheet",e.WindowBackground="windowBackground",e.HudWindow="hudWindow",e.FullScreenUI="fullScreenUI",e.Tooltip="tooltip",e.ContentBackground="contentBackground",e.UnderWindowBackground="underWindowBackground",e.UnderPageBackground="underPageBackground",e.Mica="mica",e.Blur="blur",e.Acrylic="acrylic",e.Tabbed="tabbed",e.TabbedDark="tabbedDark",e.TabbedLight="tabbedLight"}(ce||(ce={})),function(e){e.FollowsWindowActiveState="followsWindowActiveState",e.Active="active",e.Inactive="inactive"}(de||(de={}));var he=Object.freeze({__proto__:null,CloseRequestedEvent:ie,get Effect(){return ce},get EffectState(){return de},LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,get ProgressBarStatus(){return ne},get UserAttentionType(){return ee},Window:le,availableMonitors:async function(){return h("plugin:window|available_monitors").then(e=>e.map(pe))},currentMonitor:async function(){return h("plugin:window|current_monitor").then(pe)},cursorPosition:async function(){return h("plugin:window|cursor_position").then(e=>new I(e))},getAllWindows:se,getCurrentWindow:re,monitorFromPoint:async function(e,n){return h("plugin:window|monitor_from_point",{x:e,y:n}).then(pe)},primaryMonitor:async function(){return h("plugin:window|primary_monitor").then(pe)}});function we(){return new ge(re(),window.__TAURI_INTERNALS__.metadata.currentWebview.label,{skip:!0})}async function _e(){return h("plugin:webview|get_all_webviews").then(e=>e.map(e=>new ge(new le(e.windowLabel,{skip:!0}),e.label,{skip:!0})))}const ye=["tauri://created","tauri://error"];class ge{constructor(e,n,t){this.window=e,this.label=n,this.listeners=Object.create(null),(null==t?void 0:t.skip)||h("plugin:webview|create_webview",{windowLabel:e.label,options:{...t,label:n}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await _e()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return we()}static async getAll(){return _e()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Webview",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Webview",label:this.label}})}async emit(e,n){if(!ye.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ye.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ye.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async position(){return h("plugin:webview|webview_position",{label:this.label}).then(e=>new I(e))}async size(){return h("plugin:webview|webview_size",{label:this.label}).then(e=>new k(e))}async close(){return h("plugin:webview|webview_close",{label:this.label})}async setSize(e){return h("plugin:webview|set_webview_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setPosition(e){return h("plugin:webview|set_webview_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFocus(){return h("plugin:webview|set_webview_focus",{label:this.label})}async setAutoResize(e){return h("plugin:webview|set_webview_auto_resize",{label:this.label,value:e})}async hide(){return h("plugin:webview|webview_hide",{label:this.label})}async show(){return h("plugin:webview|webview_show",{label:this.label})}async setZoom(e){return h("plugin:webview|set_webview_zoom",{label:this.label,value:e})}async reparent(e){return h("plugin:webview|reparent",{label:this.label,window:"string"==typeof e?e:e.label})}async clearAllBrowsingData(){return h("plugin:webview|clear_all_browsing_data")}async setBackgroundColor(e){return h("plugin:webview|set_webview_background_color",{color:e})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}}var be,me,fe=Object.freeze({__proto__:null,Webview:ge,getAllWebviews:_e,getCurrentWebview:we});function ve(){const e=we();return new Ae(e.label,{skip:!0})}async function ke(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new Ae(e,{skip:!0})))}class Ae{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:webview|create_webview_window",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;const t=null!==(n=(await ke()).find(n=>n.label===e))&&void 0!==n?n:null;return t?new Ae(t.label,{skip:!0}):null}static getCurrent(){return ve()}static async getAll(){return ke()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e}).then(()=>h("plugin:webview|set_webview_background_color",{color:e}))}}be=Ae,me=[le,ge],(Array.isArray(me)?me:[me]).forEach(e=>{Object.getOwnPropertyNames(e.prototype).forEach(n=>{var t;"object"==typeof be.prototype&&be.prototype&&n in be.prototype||Object.defineProperty(be.prototype,n,null!==(t=Object.getOwnPropertyDescriptor(e.prototype,n))&&void 0!==t?t:Object.create(null))})});var Te=Object.freeze({__proto__:null,WebviewWindow:Ae,getAllWebviewWindows:ke,getCurrentWebviewWindow:ve});return e.app=f,e.core=_,e.dpi=D,e.event=O,e.image=m,e.menu=J,e.mocks=K,e.path=Y,e.tray=te,e.webview=fe,e.webviewWindow=Te,e.window=he,e}({});window.__TAURI__=__TAURI_IIFE__; +var __TAURI_IIFE__=function(e){"use strict";function n(e,n,t,i){if("a"===t&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof n?e!==n||!i:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?i:"a"===t?i.call(e):i?i.value:n.get(e)}function t(e,n,t,i,r){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?r.call(e,t):r?r.value=t:n.set(e,t),t}var i,r,s,a,l;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";function u(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}class c{constructor(e){i.set(this,void 0),r.set(this,0),s.set(this,[]),a.set(this,void 0),t(this,i,e||(()=>{}),"f"),this.id=u(e=>{const l=e.index;if("end"in e)return void(l==n(this,r,"f")?this.cleanupCallback():t(this,a,l,"f"));const o=e.message;if(l==n(this,r,"f")){for(n(this,i,"f").call(this,o),t(this,r,n(this,r,"f")+1,"f");n(this,r,"f")in n(this,s,"f");){const e=n(this,s,"f")[n(this,r,"f")];n(this,i,"f").call(this,e),delete n(this,s,"f")[n(this,r,"f")],t(this,r,n(this,r,"f")+1,"f")}n(this,r,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,s,"f")[l]=o})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){t(this,i,e,"f")}get onmessage(){return n(this,i,"f")}[(i=new WeakMap,r=new WeakMap,s=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}class d{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return h(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function p(e,n,t){const i=new c(t);try{return await h(`plugin:${e}|register_listener`,{event:n,handler:i}),new d(e,n,i.id)}catch{return await h(`plugin:${e}|registerListener`,{event:n,handler:i}),new d(e,n,i.id)}}async function h(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}class w{get rid(){return n(this,l,"f")}constructor(e){l.set(this,void 0),t(this,l,e,"f")}async close(){return h("plugin:resources|close",{rid:this.rid})}}l=new WeakMap;var _=Object.freeze({__proto__:null,Channel:c,PluginListener:d,Resource:w,SERIALIZE_TO_IPC_FN:o,addPluginListener:p,checkPermissions:async function(e){return h(`plugin:${e}|check_permissions`)},convertFileSrc:function(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)},invoke:h,isTauri:function(){return!!(globalThis||window).isTauri},requestPermissions:async function(e){return h(`plugin:${e}|request_permissions`)},transformCallback:u});class y extends w{constructor(e){super(e)}static async new(e,n,t){return h("plugin:image|new",{rgba:g(e),width:n,height:t}).then(e=>new y(e))}static async fromBytes(e){return h("plugin:image|from_bytes",{bytes:g(e)}).then(e=>new y(e))}static async fromPath(e){return h("plugin:image|from_path",{path:e}).then(e=>new y(e))}async rgba(){return h("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return h("plugin:image|size",{rid:this.rid})}}function g(e){return null==e?null:"string"==typeof e?e:e instanceof y?e.rid:e}var b,m=Object.freeze({__proto__:null,Image:y,transformImage:g});!function(e){e.Nsis="nsis",e.Msi="msi",e.Deb="deb",e.Rpm="rpm",e.AppImage="appimage",e.App="app"}(b||(b={}));var f=Object.freeze({__proto__:null,get BundleType(){return b},defaultWindowIcon:async function(){return h("plugin:app|default_window_icon").then(e=>e?new y(e):null)},fetchDataStoreIdentifiers:async function(){return h("plugin:app|fetch_data_store_identifiers")},getBundleType:async function(){return h("plugin:app|bundle_type")},getIdentifier:async function(){return h("plugin:app|identifier")},getName:async function(){return h("plugin:app|name")},getTauriVersion:async function(){return h("plugin:app|tauri_version")},getVersion:async function(){return h("plugin:app|version")},hide:async function(){return h("plugin:app|app_hide")},onBackButtonPress:async function(e){return p("app","back-button",e)},removeDataStore:async function(e){return h("plugin:app|remove_data_store",{uuid:e})},setDockVisibility:async function(e){return h("plugin:app|set_dock_visibility",{visible:e})},setTheme:async function(e){return h("plugin:app|set_app_theme",{theme:e})},show:async function(){return h("plugin:app|app_show")},supportsMultipleWindows:async function(){return h("plugin:app|supports_multiple_windows")}});class v{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.width=e[0].Logical.width,this.height=e[0].Logical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toPhysical(e){return new k(this.width*e,this.height*e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class k{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.width=e[0].Physical.width,this.height=e[0].Physical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toLogical(e){return new v(this.width/e,this.height/e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class A{constructor(e){this.size=e}toLogical(e){return this.size instanceof v?this.size:this.size.toLogical(e)}toPhysical(e){return this.size instanceof k?this.size:this.size.toPhysical(e)}[o](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[o]()}}class T{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.x=e[0].Logical.x,this.y=e[0].Logical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toPhysical(e){return new I(this.x*e,this.y*e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class I{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.x=e[0].Physical.x,this.y=e[0].Physical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toLogical(e){return new T(this.x/e,this.y/e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class E{constructor(e){this.position=e}toLogical(e){return this.position instanceof T?this.position:this.position.toLogical(e)}toPhysical(e){return this.position instanceof I?this.position:this.position.toPhysical(e)}[o](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[o]()}}var R,D=Object.freeze({__proto__:null,LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,Position:E,Size:A});async function S(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await h("plugin:event|unlisten",{event:e,eventId:n})}async function N(e,n,t){var i;const r="string"==typeof(null==t?void 0:t.target)?{kind:"AnyLabel",label:t.target}:null!==(i=null==t?void 0:t.target)&&void 0!==i?i:{kind:"Any"};return h("plugin:event|listen",{event:e,target:r,handler:u(n)}).then(n=>async()=>S(e,n))}async function L(e,n,t){return N(e,t=>{S(e,t.id),n(t)},t)}async function C(e,n){await h("plugin:event|emit",{event:e,payload:n})}async function x(e,n,t){const i="string"==typeof e?{kind:"AnyLabel",label:e}:e;await h("plugin:event|emit_to",{target:i,event:n,payload:t})}!function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_SUSPENDED="tauri://suspended",e.WINDOW_RESUMED="tauri://resumed",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(R||(R={}));var W,P,z,O=Object.freeze({__proto__:null,get TauriEvent(){return R},emit:C,emitTo:x,listen:N,once:L});function U(e){var n;if("items"in e)e.items=null===(n=e.items)||void 0===n?void 0:n.map(e=>"rid"in e?e:U(e));else if("action"in e&&e.action){const n=new c;return n.onmessage=e.action,delete e.action,{...e,handler:n}}return e}async function F(e,n){const t=new c;if(n&&"object"==typeof n&&("action"in n&&n.action&&(t.onmessage=n.action,delete n.action),"item"in n&&n.item&&"object"==typeof n.item&&"About"in n.item&&n.item.About&&"object"==typeof n.item.About&&"icon"in n.item.About&&n.item.About.icon&&(n.item.About.icon=g(n.item.About.icon)),"icon"in n&&n.icon&&(n.icon=g(n.icon)),"items"in n&&n.items)){function i(e){var n;return"rid"in e?[e.rid,e.kind]:("item"in e&&"object"==typeof e.item&&(null===(n=e.item.About)||void 0===n?void 0:n.icon)&&(e.item.About.icon=g(e.item.About.icon)),"icon"in e&&e.icon&&(e.icon=g(e.icon)),"items"in e&&e.items&&(e.items=e.items.map(i)),U(e))}n.items=n.items.map(i)}return h("plugin:menu|new",{kind:e,options:n,handler:t})}class M extends w{get id(){return n(this,W,"f")}get kind(){return n(this,P,"f")}constructor(e,n,i){super(e),W.set(this,void 0),P.set(this,void 0),t(this,W,n,"f"),t(this,P,i,"f")}}W=new WeakMap,P=new WeakMap;class B extends M{constructor(e,n){super(e,n,"MenuItem")}static async new(e){return F("MenuItem",e).then(([e,n])=>new B(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}}class V extends M{constructor(e,n){super(e,n,"Check")}static async new(e){return F("Check",e).then(([e,n])=>new V(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async isChecked(){return h("plugin:menu|is_checked",{rid:this.rid})}async setChecked(e){return h("plugin:menu|set_checked",{rid:this.rid,checked:e})}}!function(e){e.Add="Add",e.Advanced="Advanced",e.Bluetooth="Bluetooth",e.Bookmarks="Bookmarks",e.Caution="Caution",e.ColorPanel="ColorPanel",e.ColumnView="ColumnView",e.Computer="Computer",e.EnterFullScreen="EnterFullScreen",e.Everyone="Everyone",e.ExitFullScreen="ExitFullScreen",e.FlowView="FlowView",e.Folder="Folder",e.FolderBurnable="FolderBurnable",e.FolderSmart="FolderSmart",e.FollowLinkFreestanding="FollowLinkFreestanding",e.FontPanel="FontPanel",e.GoLeft="GoLeft",e.GoRight="GoRight",e.Home="Home",e.IChatTheater="IChatTheater",e.IconView="IconView",e.Info="Info",e.InvalidDataFreestanding="InvalidDataFreestanding",e.LeftFacingTriangle="LeftFacingTriangle",e.ListView="ListView",e.LockLocked="LockLocked",e.LockUnlocked="LockUnlocked",e.MenuMixedState="MenuMixedState",e.MenuOnState="MenuOnState",e.MobileMe="MobileMe",e.MultipleDocuments="MultipleDocuments",e.Network="Network",e.Path="Path",e.PreferencesGeneral="PreferencesGeneral",e.QuickLook="QuickLook",e.RefreshFreestanding="RefreshFreestanding",e.Refresh="Refresh",e.Remove="Remove",e.RevealFreestanding="RevealFreestanding",e.RightFacingTriangle="RightFacingTriangle",e.Share="Share",e.Slideshow="Slideshow",e.SmartBadge="SmartBadge",e.StatusAvailable="StatusAvailable",e.StatusNone="StatusNone",e.StatusPartiallyAvailable="StatusPartiallyAvailable",e.StatusUnavailable="StatusUnavailable",e.StopProgressFreestanding="StopProgressFreestanding",e.StopProgress="StopProgress",e.TrashEmpty="TrashEmpty",e.TrashFull="TrashFull",e.User="User",e.UserAccounts="UserAccounts",e.UserGroup="UserGroup",e.UserGuest="UserGuest"}(z||(z={}));class G extends M{constructor(e,n){super(e,n,"Icon")}static async new(e){return F("Icon",e).then(([e,n])=>new G(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class j extends M{constructor(e,n){super(e,n,"Predefined")}static async new(e){return F("Predefined",e).then(([e,n])=>new j(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}}function H([e,n,t]){switch(t){case"Submenu":return new $(e,n);case"Predefined":return new j(e,n);case"Check":return new V(e,n);case"Icon":return new G(e,n);default:return new B(e,n)}}class $ extends M{constructor(e,n){super(e,n,"Submenu")}static async new(e){return F("Submenu",e).then(([e,n])=>new $(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsWindowsMenuForNSApp(){return h("plugin:menu|set_as_windows_menu_for_nsapp",{rid:this.rid})}async setAsHelpMenuForNSApp(){return h("plugin:menu|set_as_help_menu_for_nsapp",{rid:this.rid})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class q extends M{constructor(e,n){super(e,n,"Menu")}static async new(e){return F("Menu",e).then(([e,n])=>new q(e,n))}static async default(){return h("plugin:menu|create_default").then(([e,n])=>new q(e,n))}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsAppMenu(){return h("plugin:menu|set_as_app_menu",{rid:this.rid}).then(e=>e?new q(e[0],e[1]):null)}async setAsWindowMenu(e){var n;return h("plugin:menu|set_as_window_menu",{rid:this.rid,window:null!==(n=null==e?void 0:e.label)&&void 0!==n?n:null}).then(e=>e?new q(e[0],e[1]):null)}}var J=Object.freeze({__proto__:null,CheckMenuItem:V,IconMenuItem:G,Menu:q,MenuItem:B,get NativeIcon(){return z},PredefinedMenuItem:j,Submenu:$,itemFromKind:H});function Q(){var e,n;window.__TAURI_INTERNALS__=null!==(e=window.__TAURI_INTERNALS__)&&void 0!==e?e:{},window.__TAURI_EVENT_PLUGIN_INTERNALS__=null!==(n=window.__TAURI_EVENT_PLUGIN_INTERNALS__)&&void 0!==n?n:{}}var Z,K=Object.freeze({__proto__:null,clearMocks:function(){"object"==typeof window.__TAURI_INTERNALS__&&(delete window.__TAURI_INTERNALS__.invoke,delete window.__TAURI_INTERNALS__.transformCallback,delete window.__TAURI_INTERNALS__.unregisterCallback,delete window.__TAURI_INTERNALS__.runCallback,delete window.__TAURI_INTERNALS__.callbacks,delete window.__TAURI_INTERNALS__.convertFileSrc,delete window.__TAURI_INTERNALS__.metadata,"object"==typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__&&delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener)},mockConvertFileSrc:function(e){Q(),window.__TAURI_INTERNALS__.convertFileSrc=function(n,t="asset"){const i=encodeURIComponent(n);return"windows"===e?`http://${t}.localhost/${i}`:`${t}://localhost/${i}`}},mockIPC:function(e,n){function t(e,n){switch(e){case"plugin:event|listen":return function(e){i.has(e.event)||i.set(e.event,[]);return i.get(e.event).push(e.handler),e.handler}(n);case"plugin:event|emit":return function(e){const n=i.get(e.event)||[];for(const t of n)a(t,e);return null}(n);case"plugin:event|unlisten":return function(e){const n=i.get(e.event);if(n){const t=n.indexOf(e.id);-1!==t&&n.splice(t,1)}}(n)}}Q();const i=new Map,r=new Map;function s(e){r.delete(e)}function a(e,n){const t=r.get(e);t?t(n):console.warn(`[TAURI] Couldn't find callback id ${e}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)}window.__TAURI_INTERNALS__.invoke=async function(i,r,s){return(null==n?void 0:n.shouldMockEvents)&&function(e){return e.startsWith("plugin:event|")}(i)?t(i,r):e(i,r)},window.__TAURI_INTERNALS__.transformCallback=function(e,n=!1){const t=window.crypto.getRandomValues(new Uint32Array(1))[0];return r.set(t,i=>(n&&s(t),e&&e(i))),t},window.__TAURI_INTERNALS__.unregisterCallback=s,window.__TAURI_INTERNALS__.runCallback=a,window.__TAURI_INTERNALS__.callbacks=r,window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener=function(e,n){s(n)}},mockWindows:function(e,...n){Q(),window.__TAURI_INTERNALS__.metadata={currentWindow:{label:e},currentWebview:{windowLabel:e,label:e}}}});!function(e){e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Document=6]="Document",e[e.Download=7]="Download",e[e.Picture=8]="Picture",e[e.Public=9]="Public",e[e.Video=10]="Video",e[e.Resource=11]="Resource",e[e.Temp=12]="Temp",e[e.AppConfig=13]="AppConfig",e[e.AppData=14]="AppData",e[e.AppLocalData=15]="AppLocalData",e[e.AppCache=16]="AppCache",e[e.AppLog=17]="AppLog",e[e.Desktop=18]="Desktop",e[e.Executable=19]="Executable",e[e.Font=20]="Font",e[e.Home=21]="Home",e[e.Runtime=22]="Runtime",e[e.Template=23]="Template"}(Z||(Z={}));var Y=Object.freeze({__proto__:null,get BaseDirectory(){return Z},appCacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppCache})},appConfigDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppConfig})},appDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppData})},appLocalDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLocalData})},appLogDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLog})},audioDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Audio})},basename:async function(e,n){return h("plugin:path|basename",{path:e,ext:n})},cacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Cache})},configDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Config})},dataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Data})},delimiter:function(){return window.__TAURI_INTERNALS__.plugins.path.delimiter},desktopDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Desktop})},dirname:async function(e){return h("plugin:path|dirname",{path:e})},documentDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Document})},downloadDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Download})},executableDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Executable})},extname:async function(e){return h("plugin:path|extname",{path:e})},fontDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Font})},homeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Home})},isAbsolute:async function(e){return h("plugin:path|is_absolute",{path:e})},join:async function(...e){return h("plugin:path|join",{paths:e})},localDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.LocalData})},normalize:async function(e){return h("plugin:path|normalize",{path:e})},pictureDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Picture})},publicDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Public})},resolve:async function(...e){return h("plugin:path|resolve",{paths:e})},resolveResource:async function(e){return h("plugin:path|resolve_directory",{directory:Z.Resource,path:e})},resourceDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Resource})},runtimeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Runtime})},sep:function(){return window.__TAURI_INTERNALS__.plugins.path.sep},tempDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Temp})},templateDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Template})},videoDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Video})}});class X extends w{constructor(e,n){super(e),this.id=n}static async getById(e){return h("plugin:tray|get_by_id",{id:e}).then(n=>n?new X(n,e):null)}static async removeById(e){return h("plugin:tray|remove_by_id",{id:e})}static async new(e){(null==e?void 0:e.menu)&&(e.menu=[e.menu.rid,e.menu.kind]),(null==e?void 0:e.icon)&&(e.icon=g(e.icon));const n=new c;if(null==e?void 0:e.action){const t=e.action;n.onmessage=e=>t(function(e){const n=e;return n.position=new I(e.position),n.rect.position=new I(e.rect.position),n.rect.size=new k(e.rect.size),n}(e)),delete e.action}return h("plugin:tray|new",{options:null!=e?e:{},handler:n}).then(([e,n])=>new X(e,n))}async setIcon(e){let n=null;return e&&(n=g(e)),h("plugin:tray|set_icon",{rid:this.rid,icon:n})}async setMenu(e){return e&&(e=[e.rid,e.kind]),h("plugin:tray|set_menu",{rid:this.rid,menu:e})}async setTooltip(e){return h("plugin:tray|set_tooltip",{rid:this.rid,tooltip:e})}async setTitle(e){return h("plugin:tray|set_title",{rid:this.rid,title:e})}async setVisible(e){return h("plugin:tray|set_visible",{rid:this.rid,visible:e})}async setTempDirPath(e){return h("plugin:tray|set_temp_dir_path",{rid:this.rid,path:e})}async setIconAsTemplate(e){return h("plugin:tray|set_icon_as_template",{rid:this.rid,asTemplate:e})}async setIconWithAsTemplate(e,n){let t=null;return e&&(t=g(e)),h("plugin:tray|set_icon_with_as_template",{rid:this.rid,icon:t,asTemplate:n})}async setMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}async setShowMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}}var ee,ne,te=Object.freeze({__proto__:null,TrayIcon:X});!function(e){e[e.Critical=1]="Critical",e[e.Informational=2]="Informational"}(ee||(ee={}));class ie{constructor(e){this._preventDefault=!1,this.event=e.event,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}function re(){return new le(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function se(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new le(e,{skip:!0})))}!function(e){e.None="none",e.Normal="normal",e.Indeterminate="indeterminate",e.Paused="paused",e.Error="error"}(ne||(ne={}));const ae=["tauri://created","tauri://error"];class le{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:window|create",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await se()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return re()}static async getAll(){return se()}static async getFocusedWindow(){for(const e of await se())if(await e.isFocused())return e;return null}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Window",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Window",label:this.label}})}async emit(e,n){if(!ae.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ae.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ae.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async scaleFactor(){return h("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return h("plugin:window|inner_position",{label:this.label}).then(e=>new I(e))}async outerPosition(){return h("plugin:window|outer_position",{label:this.label}).then(e=>new I(e))}async innerSize(){return h("plugin:window|inner_size",{label:this.label}).then(e=>new k(e))}async outerSize(){return h("plugin:window|outer_size",{label:this.label}).then(e=>new k(e))}async isFullscreen(){return h("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return h("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return h("plugin:window|is_maximized",{label:this.label})}async isFocused(){return h("plugin:window|is_focused",{label:this.label})}async isDecorated(){return h("plugin:window|is_decorated",{label:this.label})}async isResizable(){return h("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return h("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return h("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return h("plugin:window|is_closable",{label:this.label})}async isVisible(){return h("plugin:window|is_visible",{label:this.label})}async title(){return h("plugin:window|title",{label:this.label})}async theme(){return h("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return h("plugin:window|is_always_on_top",{label:this.label})}async activityName(){return h("plugin:window|activity_name",{label:this.label})}async sceneIdentifier(){return h("plugin:window|scene_identifier",{label:this.label})}async center(){return h("plugin:window|center",{label:this.label})}async requestUserAttention(e){let n=null;return e&&(n=e===ee.Critical?{type:"Critical"}:{type:"Informational"}),h("plugin:window|request_user_attention",{label:this.label,value:n})}async setResizable(e){return h("plugin:window|set_resizable",{label:this.label,value:e})}async setEnabled(e){return h("plugin:window|set_enabled",{label:this.label,value:e})}async isEnabled(){return h("plugin:window|is_enabled",{label:this.label})}async setMaximizable(e){return h("plugin:window|set_maximizable",{label:this.label,value:e})}async setMinimizable(e){return h("plugin:window|set_minimizable",{label:this.label,value:e})}async setClosable(e){return h("plugin:window|set_closable",{label:this.label,value:e})}async setTitle(e){return h("plugin:window|set_title",{label:this.label,value:e})}async maximize(){return h("plugin:window|maximize",{label:this.label})}async unmaximize(){return h("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return h("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return h("plugin:window|minimize",{label:this.label})}async unminimize(){return h("plugin:window|unminimize",{label:this.label})}async show(){return h("plugin:window|show",{label:this.label})}async hide(){return h("plugin:window|hide",{label:this.label})}async close(){return h("plugin:window|close",{label:this.label})}async destroy(){return h("plugin:window|destroy",{label:this.label})}async setDecorations(e){return h("plugin:window|set_decorations",{label:this.label,value:e})}async setShadow(e){return h("plugin:window|set_shadow",{label:this.label,value:e})}async setEffects(e){return h("plugin:window|set_effects",{label:this.label,value:e})}async clearEffects(){return h("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(e){return h("plugin:window|set_always_on_top",{label:this.label,value:e})}async setAlwaysOnBottom(e){return h("plugin:window|set_always_on_bottom",{label:this.label,value:e})}async setContentProtected(e){return h("plugin:window|set_content_protected",{label:this.label,value:e})}async setSize(e){return h("plugin:window|set_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setMinSize(e){return h("plugin:window|set_min_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setMaxSize(e){return h("plugin:window|set_max_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setSizeConstraints(e){function n(e){return e?{Logical:e}:null}return h("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:n(null==e?void 0:e.minWidth),minHeight:n(null==e?void 0:e.minHeight),maxWidth:n(null==e?void 0:e.maxWidth),maxHeight:n(null==e?void 0:e.maxHeight)}})}async setPosition(e){return h("plugin:window|set_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFullscreen(e){return h("plugin:window|set_fullscreen",{label:this.label,value:e})}async setSimpleFullscreen(e){return h("plugin:window|set_simple_fullscreen",{label:this.label,value:e})}async setFocus(){return h("plugin:window|set_focus",{label:this.label})}async setFocusable(e){return h("plugin:window|set_focusable",{label:this.label,value:e})}async setIcon(e){return h("plugin:window|set_icon",{label:this.label,value:g(e)})}async setSkipTaskbar(e){return h("plugin:window|set_skip_taskbar",{label:this.label,value:e})}async setCursorGrab(e){return h("plugin:window|set_cursor_grab",{label:this.label,value:e})}async setCursorVisible(e){return h("plugin:window|set_cursor_visible",{label:this.label,value:e})}async setCursorIcon(e){return h("plugin:window|set_cursor_icon",{label:this.label,value:e})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e})}async setCursorPosition(e){return h("plugin:window|set_cursor_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setIgnoreCursorEvents(e){return h("plugin:window|set_ignore_cursor_events",{label:this.label,value:e})}async startDragging(){return h("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(e){return h("plugin:window|start_resize_dragging",{label:this.label,value:e})}async setBadgeCount(e){return h("plugin:window|set_badge_count",{label:this.label,value:e})}async setBadgeLabel(e){return h("plugin:window|set_badge_label",{label:this.label,value:e})}async setOverlayIcon(e){return h("plugin:window|set_overlay_icon",{label:this.label,value:e?g(e):void 0})}async setProgressBar(e){return h("plugin:window|set_progress_bar",{label:this.label,value:e})}async setVisibleOnAllWorkspaces(e){return h("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:e})}async setTitleBarStyle(e){return h("plugin:window|set_title_bar_style",{label:this.label,value:e})}async setTheme(e){return h("plugin:window|set_theme",{label:this.label,value:e})}async onResized(e){return this.listen(R.WINDOW_RESIZED,n=>{n.payload=new k(n.payload),e(n)})}async onMoved(e){return this.listen(R.WINDOW_MOVED,n=>{n.payload=new I(n.payload),e(n)})}async onCloseRequested(e){return this.listen(R.WINDOW_CLOSE_REQUESTED,async n=>{const t=new ie(n);await e(t),t.isPreventDefault()||await this.destroy()})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}async onFocusChanged(e){const n=await this.listen(R.WINDOW_FOCUS,n=>{e({...n,payload:!0})}),t=await this.listen(R.WINDOW_BLUR,n=>{e({...n,payload:!1})});return()=>{n(),t()}}async onScaleChanged(e){return this.listen(R.WINDOW_SCALE_FACTOR_CHANGED,e)}async onThemeChanged(e){return this.listen(R.WINDOW_THEME_CHANGED,e)}}var oe,ue,ce,de;function pe(e){return null===e?null:{name:e.name,scaleFactor:e.scaleFactor,position:new I(e.position),size:new k(e.size),workArea:{position:new I(e.workArea.position),size:new k(e.workArea.size)}}}!function(e){e.Disabled="disabled",e.Throttle="throttle",e.Suspend="suspend"}(oe||(oe={})),function(e){e.Default="default",e.FluentOverlay="fluentOverlay"}(ue||(ue={})),function(e){e.AppearanceBased="appearanceBased",e.Light="light",e.Dark="dark",e.MediumLight="mediumLight",e.UltraDark="ultraDark",e.Titlebar="titlebar",e.Selection="selection",e.Menu="menu",e.Popover="popover",e.Sidebar="sidebar",e.HeaderView="headerView",e.Sheet="sheet",e.WindowBackground="windowBackground",e.HudWindow="hudWindow",e.FullScreenUI="fullScreenUI",e.Tooltip="tooltip",e.ContentBackground="contentBackground",e.UnderWindowBackground="underWindowBackground",e.UnderPageBackground="underPageBackground",e.Mica="mica",e.Blur="blur",e.Acrylic="acrylic",e.Tabbed="tabbed",e.TabbedDark="tabbedDark",e.TabbedLight="tabbedLight"}(ce||(ce={})),function(e){e.FollowsWindowActiveState="followsWindowActiveState",e.Active="active",e.Inactive="inactive"}(de||(de={}));var he=Object.freeze({__proto__:null,CloseRequestedEvent:ie,get Effect(){return ce},get EffectState(){return de},LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,get ProgressBarStatus(){return ne},get UserAttentionType(){return ee},Window:le,availableMonitors:async function(){return h("plugin:window|available_monitors").then(e=>e.map(pe))},currentMonitor:async function(){return h("plugin:window|current_monitor").then(pe)},cursorPosition:async function(){return h("plugin:window|cursor_position").then(e=>new I(e))},getAllWindows:se,getCurrentWindow:re,monitorFromPoint:async function(e,n){return h("plugin:window|monitor_from_point",{x:e,y:n}).then(pe)},primaryMonitor:async function(){return h("plugin:window|primary_monitor").then(pe)}});function we(){return new ge(re(),window.__TAURI_INTERNALS__.metadata.currentWebview.label,{skip:!0})}async function _e(){return h("plugin:webview|get_all_webviews").then(e=>e.map(e=>new ge(new le(e.windowLabel,{skip:!0}),e.label,{skip:!0})))}const ye=["tauri://created","tauri://error"];class ge{constructor(e,n,t){this.window=e,this.label=n,this.listeners=Object.create(null),(null==t?void 0:t.skip)||h("plugin:webview|create_webview",{windowLabel:e.label,options:{...t,label:n}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await _e()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return we()}static async getAll(){return _e()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Webview",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Webview",label:this.label}})}async emit(e,n){if(!ye.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ye.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ye.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async position(){return h("plugin:webview|webview_position",{label:this.label}).then(e=>new I(e))}async size(){return h("plugin:webview|webview_size",{label:this.label}).then(e=>new k(e))}async close(){return h("plugin:webview|webview_close",{label:this.label})}async setSize(e){return h("plugin:webview|set_webview_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setPosition(e){return h("plugin:webview|set_webview_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFocus(){return h("plugin:webview|set_webview_focus",{label:this.label})}async setAutoResize(e){return h("plugin:webview|set_webview_auto_resize",{label:this.label,value:e})}async hide(){return h("plugin:webview|webview_hide",{label:this.label})}async show(){return h("plugin:webview|webview_show",{label:this.label})}async setZoom(e){return h("plugin:webview|set_webview_zoom",{label:this.label,value:e})}async reparent(e){return h("plugin:webview|reparent",{label:this.label,window:"string"==typeof e?e:e.label})}async clearAllBrowsingData(){return h("plugin:webview|clear_all_browsing_data")}async setBackgroundColor(e){return h("plugin:webview|set_webview_background_color",{color:e})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}}var be,me,fe=Object.freeze({__proto__:null,Webview:ge,getAllWebviews:_e,getCurrentWebview:we});function ve(){const e=we();return new Ae(e.label,{skip:!0})}async function ke(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new Ae(e,{skip:!0})))}class Ae{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:webview|create_webview_window",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;const t=null!==(n=(await ke()).find(n=>n.label===e))&&void 0!==n?n:null;return t?new Ae(t.label,{skip:!0}):null}static getCurrent(){return ve()}static async getAll(){return ke()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e}).then(()=>h("plugin:webview|set_webview_background_color",{color:e}))}}be=Ae,me=[le,ge],(Array.isArray(me)?me:[me]).forEach(e=>{Object.getOwnPropertyNames(e.prototype).forEach(n=>{var t;"object"==typeof be.prototype&&be.prototype&&n in be.prototype||Object.defineProperty(be.prototype,n,null!==(t=Object.getOwnPropertyDescriptor(e.prototype,n))&&void 0!==t?t:Object.create(null))})});var Te=Object.freeze({__proto__:null,WebviewWindow:Ae,getAllWebviewWindows:ke,getCurrentWebviewWindow:ve});return e.app=f,e.core=_,e.dpi=D,e.event=O,e.image=m,e.menu=J,e.mocks=K,e.path=Y,e.tray=te,e.webview=fe,e.webviewWindow=Te,e.window=he,e}({});window.__TAURI__=__TAURI_IIFE__; diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index 47cc6d71fb66..0384f9ef763f 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -42,6 +42,7 @@ use std::{ borrow::Cow, collections::HashMap, fmt, + path::Path, sync::{Arc, Mutex, MutexGuard, atomic, mpsc::Sender}, thread::ThreadId, time::Duration, @@ -67,6 +68,9 @@ pub type SetupHook = Box) -> std::result::Result<(), Box> + Send>; /// A closure that is run every time a page starts or finishes loading. pub type OnPageLoad = dyn Fn(&Webview, &PageLoadPayload<'_>) + Send + Sync + 'static; +/// A closure that is run when the web content process terminates. +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub type OnWebContentProcessTerminate = dyn Fn(&Webview) + Send + Sync + 'static; pub type ChannelInterceptor = Box, CallbackFn, usize, &InvokeResponseBody) -> bool + Send + Sync + 'static>; @@ -146,6 +150,24 @@ pub enum WindowEvent { /// /// - **Linux**: Not supported. ThemeChanged(Theme), + /// Emitted when the application has been suspended. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onPause` method of the Activity. + /// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Suspended, + /// Emitted when the application has been resumed. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation. + /// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Resumed, } impl From for WindowEvent { @@ -167,6 +189,10 @@ impl From for WindowEvent { }, RuntimeWindowEvent::DragDrop(event) => Self::DragDrop(event), RuntimeWindowEvent::ThemeChanged(theme) => Self::ThemeChanged(theme), + #[cfg(mobile)] + RuntimeWindowEvent::Suspended => Self::Suspended, + #[cfg(mobile)] + RuntimeWindowEvent::Resumed => Self::Resumed, } } } @@ -250,6 +276,20 @@ pub enum RunEvent { /// Indicates whether the NSApplication object found any visible windows in your application. has_visible_windows: bool, }, + /// Emitted when a scene is requested by the system. + /// + /// This event is emitted when a scene is requested by the system. + /// Scenes created by [`Window::new`] are not emitted with this event. + /// It is also not emitted for the main scene. + #[cfg(target_os = "ios")] + SceneRequested { + /// Scene that was requested by the system. + scene: objc2::rc::Retained, + /// Options that were used to request the scene. + /// + /// This lets you determine why the scene was requested. + options: objc2::rc::Retained, + }, } impl From for RunEvent { @@ -263,7 +303,7 @@ impl From for RunEvent { } } -/// The asset resolver is a helper to access the [`tauri_utils::assets::Assets`] interface. +/// The asset resolver is a helper to access the [`crate::Assets`] interface. #[derive(Debug, Clone)] pub struct AssetResolver { manager: Arc>, @@ -628,6 +668,18 @@ impl AppHandle { pub fn set_device_event_filter(&self, filter: DeviceEventFilter) { self.runtime_handle.set_device_event_filter(filter); } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + let (tx, rx) = std::sync::mpsc::channel(); + self.run_on_main_thread(move || unsafe { + let mtm = objc2::MainThreadMarker::new().unwrap(); + let ui_application = objc2_ui_kit::UIApplication::sharedApplication(mtm); + tx.send(ui_application.supportsMultipleScenes()).unwrap(); + }); + rx.recv().unwrap() + } } impl Manager for AppHandle { @@ -652,6 +704,16 @@ impl ManagerBase for AppHandle { fn managed_app_handle(&self) -> &AppHandle { self } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + None + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + None + } } /// The instance of the currently running application. @@ -702,6 +764,16 @@ impl ManagerBase for App { fn managed_app_handle(&self) -> &AppHandle { self.handle() } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + None + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + None + } } /// APIs specific to the wry runtime. @@ -1041,6 +1113,40 @@ macro_rules! shared_app_impl { pub fn invoke_key(&self) -> &str { self.manager.invoke_key() } + + /// Whether the application supports multiple windows. + #[cfg(desktop)] + pub fn supports_multiple_windows(&self) -> bool { + true + } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "android")] + pub fn supports_multiple_windows(&self) -> bool { + let runtime_handle = match self.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.handle(), + RuntimeOrDispatch::RuntimeHandle(handle) => handle, + _ => unreachable!(), + }; + + let (tx, rx) = std::sync::mpsc::channel(); + + runtime_handle.run_on_android_context(move |env, _activity, _webview| { + let supports = (|| { + let version_class = env.find_class("android/os/Build$VERSION")?; + let sdk = env + .get_static_field(version_class, "SDK_INT", "I")? + .i() + .unwrap_or_default(); + crate::Result::Ok(sdk >= 32) + })() + .unwrap_or(false); + + let _ = tx.send(supports); + }); + + rx.recv().unwrap_or(false) + } } impl Listener for $app { @@ -1140,6 +1246,16 @@ impl App { &self.handle } + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + unsafe { + let mtm = objc2::MainThreadMarker::new().unwrap(); + let ui_application = objc2_ui_kit::UIApplication::sharedApplication(mtm); + ui_application.supportsMultipleScenes() + } + } + /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. /// /// # Examples @@ -1386,6 +1502,10 @@ pub struct Builder { /// Page load hook. on_page_load: Option>>, + /// Web content process termination hook. + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate: Option>>, + /// All passed plugins plugins: PluginStore, @@ -1483,6 +1603,8 @@ impl Builder { .into_string(), channel_interceptor: None, on_page_load: None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate: None, plugins: PluginStore::default(), uri_scheme_protocols: Default::default(), state: StateManager::new(), @@ -1542,6 +1664,20 @@ impl Builder { ); self } + + /// Sets the disk cache directory for CEF (`Settings::cache_path`). + /// + /// Calling this more than once keeps the path from the last call. + /// If omitted, the cache defaults to `{user cache directory}/{identifier}/cef`. + #[cfg(feature = "cef")] + pub fn root_cache_path>(mut self, path: P) -> Self { + self + .platform_specific_attributes + .push(tauri_runtime_cef::RuntimeInitAttribute::CachePath { + path: path.as_ref().to_path_buf(), + }); + self + } } impl Builder { @@ -1738,6 +1874,23 @@ tauri::Builder::::new() self } + /// Defines the web content process termination hook. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[must_use] + pub fn on_web_content_process_terminate(mut self, on_web_content_process_terminate: F) -> Self + where + F: Fn(&Webview) + Send + Sync + 'static, + { + self + .on_web_content_process_terminate + .replace(Arc::new(on_web_content_process_terminate)); + self + } + /// Adds a Tauri application plugin. /// /// A plugin is created using the [`crate::plugin::Builder`] struct.Check its documentation for more information. @@ -2193,6 +2346,8 @@ tauri::Builder::::new() self.plugins, self.invoke_handler, self.on_page_load, + #[cfg(any(target_os = "macos", target_os = "ios"))] + self.on_web_content_process_terminate, self.uri_scheme_protocols, self.state, #[cfg(desktop)] @@ -2581,6 +2736,10 @@ fn on_event_loop_event( } => RunEvent::Reopen { has_visible_windows, }, + #[cfg(target_os = "ios")] + RuntimeRunEvent::SceneRequested { scene, options } => { + RunEvent::SceneRequested { scene, options } + } _ => unimplemented!(), }; diff --git a/crates/tauri/src/app/plugin.rs b/crates/tauri/src/app/plugin.rs index d9d32b7f114f..d550e26c00b2 100644 --- a/crates/tauri/src/app/plugin.rs +++ b/crates/tauri/src/app/plugin.rs @@ -114,6 +114,11 @@ pub fn bundle_type() -> Option { tauri_utils::platform::bundle_type() } +#[command(root = "crate")] +pub fn supports_multiple_windows(app: AppHandle) -> bool { + app.supports_multiple_windows() +} + pub fn init() -> TauriPlugin { Builder::new("app") .invoke_handler(crate::generate_handler![ @@ -130,6 +135,7 @@ pub fn init() -> TauriPlugin { set_app_theme, set_dock_visibility, bundle_type, + supports_multiple_windows, ]) .setup(|_app, _api| { #[cfg(target_os = "android")] diff --git a/crates/tauri/src/async_runtime.rs b/crates/tauri/src/async_runtime.rs index a1783e064cec..bfa80f5bd916 100644 --- a/crates/tauri/src/async_runtime.rs +++ b/crates/tauri/src/async_runtime.rs @@ -42,6 +42,7 @@ impl GlobalRuntime { } } + #[track_caller] fn spawn(&self, task: F) -> JoinHandle where F: Future + Send + 'static, @@ -54,6 +55,7 @@ impl GlobalRuntime { } } + #[track_caller] pub fn spawn_blocking(&self, func: F) -> JoinHandle where F: FnOnce() -> R + Send + 'static, @@ -66,6 +68,7 @@ impl GlobalRuntime { } } + #[track_caller] fn block_on(&self, task: F) -> F::Output { if let Some(r) = &self.runtime { r.block_on(task) @@ -95,6 +98,7 @@ impl Runtime { } } + #[track_caller] /// Spawns a future onto the runtime. pub fn spawn(&self, task: F) -> JoinHandle where @@ -109,6 +113,7 @@ impl Runtime { } } + #[track_caller] /// Runs the provided function on an executor dedicated to blocking operations. pub fn spawn_blocking(&self, func: F) -> JoinHandle where @@ -120,6 +125,7 @@ impl Runtime { } } + #[track_caller] /// Runs a future to completion on runtime. pub fn block_on(&self, task: F) -> F::Output { match self { @@ -177,6 +183,7 @@ impl RuntimeHandle { h } + #[track_caller] /// Runs the provided function on an executor dedicated to blocking operations. pub fn spawn_blocking(&self, func: F) -> JoinHandle where @@ -188,6 +195,7 @@ impl RuntimeHandle { } } + #[track_caller] /// Spawns a future onto the runtime. pub fn spawn(&self, task: F) -> JoinHandle where @@ -202,6 +210,7 @@ impl RuntimeHandle { } } + #[track_caller] /// Runs a future to completion on runtime. pub fn block_on(&self, task: F) -> F::Output { match self { @@ -258,12 +267,14 @@ pub fn handle() -> RuntimeHandle { runtime.handle() } +#[track_caller] /// Runs a future to completion on runtime. pub fn block_on(task: F) -> F::Output { let runtime = RUNTIME.get_or_init(default_runtime); runtime.block_on(task) } +#[track_caller] /// Spawns a future onto the runtime. pub fn spawn(task: F) -> JoinHandle where @@ -274,6 +285,7 @@ where runtime.spawn(task) } +#[track_caller] /// Runs the provided function on an executor dedicated to blocking operations. pub fn spawn_blocking(func: F) -> JoinHandle where @@ -284,6 +296,7 @@ where runtime.spawn_blocking(func) } +#[track_caller] #[allow(dead_code)] pub(crate) fn safe_block_on(task: F) -> F::Output where diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 9dec4165802b..c25e98de5f29 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -194,7 +194,7 @@ impl JsImage { /// This will retrieve the image from the passed [`ResourceTable`] if it is [`JsImage::Resource`] /// and will return an error if it doesn't exist in the passed [`ResourceTable`] so make sure /// the passed [`ResourceTable`] is the same one used to store the image, usually this should be - /// the webview [resources table](crate::webview::Webview::resources_table). + /// the webview resources table. pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result>> { match self { Self::Resource(rid) => resources_table.get::>(rid), diff --git a/crates/tauri/src/ipc/protocol.rs b/crates/tauri/src/ipc/protocol.rs index 3ecd1b7f4ef1..9f3860b9dee8 100644 --- a/crates/tauri/src/ipc/protocol.rs +++ b/crates/tauri/src/ipc/protocol.rs @@ -466,15 +466,11 @@ fn parse_invoke_request( (body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) .and_then(|raw| { - let content_type = raw.content_type().clone(); - crypto_keys.decrypt(raw).map(|decrypted| { - ( - decrypted, - content_type - .parse() - .unwrap_or(mime::APPLICATION_OCTET_STREAM), - ) - }) + let content_type = raw + .content_type() + .parse() + .unwrap_or(mime::APPLICATION_OCTET_STREAM); + Ok((crypto_keys.decrypt(raw)?, content_type)) }) .map_err(|e| e.to_string())?; } @@ -571,6 +567,8 @@ mod tests { PluginStore::default(), Box::new(|_| false), None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, Default::default(), StateManager::new(), Default::default(), @@ -687,6 +685,8 @@ mod tests { PluginStore::default(), Box::new(|_| false), None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, Default::default(), StateManager::new(), Default::default(), diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index afee655adf04..fde25aa012a3 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -14,6 +14,7 @@ //! - **cef**: Enables the [CEF](https://github.com/chromiumembedded/cef) runtime. // - **common-controls-v6** *(enabled by default)*: Enables [Common Controls v6](https://learn.microsoft.com/en-us/windows/win32/controls/common-control-versions) support on Windows, mainly for the predefined `about` menu item. //! - **x11** *(enabled by default)*: Enables X11 support. Disable this if you only target Wayland. +//! - **dbus** *(enabled by default)*: Enables dbus dependency for theme support on Linux. Disable this if you do not need theme support or don't want to build the dbus rust crate. The WebView dependencies use dbus either way. //! - **unstable**: Enables unstable features. Be careful, it might introduce breaking changes in future minor releases. //! - **tracing**: Enables [`tracing`](https://docs.rs/tracing/latest/tracing) for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers. //! - **test**: Enables the [`mod@test`] module exposing unit test helpers. @@ -33,10 +34,10 @@ //! - **compression** *(enabled by default): Enables asset compression. You should only disable this if you want faster compile times in release builds - it produces larger binaries. //! - **config-json5**: Adds support to JSON5 format for `tauri.conf.json`. //! - **config-toml**: Adds support to TOML format for the configuration `Tauri.toml`. -//! - **image-ico**: Adds support to parse `.ico` image, see [`Image`]. -//! - **image-png**: Adds support to parse `.png` image, see [`Image`]. +//! - **image-ico**: Adds support to parse `.ico` image, see [`image::Image`]. +//! - **image-png**: Adds support to parse `.png` image, see [`image::Image`]. //! - **macos-proxy**: Adds support for [`WebviewBuilder::proxy_url`] on macOS. Requires macOS 14+. -//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/%5E2.0.0-rc.9/specta/attr.specta.html) with Tauri arguments such as [`State`](crate::State), [`Window`](crate::Window) and [`AppHandle`](crate::AppHandle) +//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/%5E2.0.0-rc.9/specta/attr.specta.html) with Tauri arguments such as [`State`], [`Window`] and [`AppHandle`] //! - **dynamic-acl** *(enabled by default)*: Enables you to add ACLs at runtime, notably it enables the [`Manager::add_capability`] function. //! //! ## Cargo allowlist features @@ -159,14 +160,7 @@ macro_rules! android_binding { ::tauri::wry::android_binding!($domain, $app_name, $wry); - ::tauri::tao::android_binding!( - $domain, - $app_name, - WryActivity, - android_setup, - $main, - ::tauri::tao - ); + ::tauri::tao::android_binding!($domain, $app_name, Rust, android_setup, $main, ::tauri::tao); // be careful when renaming this, the `Java_app_tauri_plugin_PluginManager_handlePluginResponse` symbol is checked by the CLI ::tauri::tao::platform::android::prelude::android_fn!( @@ -223,14 +217,6 @@ use std::{ }; use utils::assets::{AssetKey, CspHash, EmbeddedAssets}; -#[cfg(feature = "wry")] -#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] -pub use tauri_runtime_wry::webview_version; - -#[cfg(feature = "cef")] -#[cfg_attr(docsrs, doc(cfg(feature = "cef")))] -pub use tauri_runtime_cef::webview_version; - #[cfg(target_os = "macos")] #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] pub use runtime::ActivationPolicy; @@ -341,6 +327,22 @@ pub const fn is_dev() -> bool { !cfg!(feature = "custom-protocol") } +// TODO: Fix the error types +/// Get WebView/Webkit version on current platform. +pub fn webview_version() -> Result { + #[cfg(feature = "cef")] + if let Ok(v) = tauri_runtime_cef::webview_version() { + return Ok(v); + } + + #[cfg(feature = "wry")] + if let Ok(v) = tauri_runtime_wry::webview_version() { + return Ok(v); + } + + Ok("0.0.0".to_string()) +} + /// Represents a container of file assets that are retrievable during runtime. pub trait Assets: Send + Sync + 'static { /// Initialize the asset provider. @@ -1093,6 +1095,10 @@ pub(crate) mod sealed { fn manager_owned(&self) -> Arc>; fn runtime(&self) -> RuntimeOrDispatch<'_, R>; fn managed_app_handle(&self) -> &AppHandle; + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option>; + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option>; } } diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs index 9f9e36b2328a..bee66cc57576 100644 --- a/crates/tauri/src/manager/mod.rs +++ b/crates/tauri/src/manager/mod.rs @@ -31,6 +31,9 @@ use crate::{ utils::{PackageInfo, config::Config}, }; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::app::OnWebContentProcessTerminate; + #[cfg(desktop)] mod menu; #[cfg(all(desktop, feature = "tray-icon"))] @@ -251,6 +254,9 @@ impl AppManager { plugins: PluginStore, invoke_handler: Box>, on_page_load: Option>>, + #[cfg(any(target_os = "macos", target_os = "ios"))] on_web_content_process_terminate: Option< + Arc>, + >, uri_scheme_protocols: HashMap>>, state: StateManager, #[cfg(desktop)] menu_event_listener: Vec>>, @@ -284,6 +290,8 @@ impl AppManager { webviews: Mutex::default(), invoke_handler, on_page_load, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate, uri_scheme_protocols: Mutex::new(uri_scheme_protocols), event_listeners: Arc::new(webview_event_listeners), invoke_initialization_script, @@ -360,6 +368,7 @@ impl AppManager { } } + // TODO: Change to return `crate::Result` here in v3 pub fn get_asset( &self, mut path: String, @@ -405,46 +414,39 @@ impl AppManager { asset_path = fallback; asset }) - .ok_or_else(|| crate::Error::AssetNotFound(path.clone())) - .map(Cow::into_owned); + .ok_or_else(|| { + let error = crate::Error::AssetNotFound(path.clone()); + log::error!("{error}"); + Box::new(error) + })?; let mut csp_header = None; let is_html = asset_path.as_ref().ends_with(".html"); - match asset_response { - Ok(asset) => { - let final_data = if is_html { - let mut asset = String::from_utf8_lossy(&asset).into_owned(); - if let Some(csp) = self.csp() { - #[allow(unused_mut)] - let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp); - #[cfg(feature = "isolation")] - if let Pattern::Isolation { schema, .. } = &*self.pattern { - let default_src = csp_map - .entry("default-src".into()) - .or_insert_with(Default::default); - default_src.push(R::custom_scheme_url(schema, _use_https_schema)); - } - - csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); - } + let final_data = if is_html { + let mut asset = String::from_utf8_lossy(&asset_response).into_owned(); + if let Some(csp) = self.csp() { + #[allow(unused_mut)] + let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp); + #[cfg(feature = "isolation")] + if let Pattern::Isolation { schema, .. } = &*self.pattern { + let default_src = csp_map.entry("default-src".to_owned()).or_default(); + default_src.push(R::custom_scheme_url(schema, _use_https_schema)); + } - asset.into_bytes() - } else { - asset - }; - let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path); - Ok(Asset { - bytes: final_data, - mime_type, - csp_header, - }) - } - Err(e) => { - log::error!("{:?}", e); - Err(Box::new(e)) + csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); } - } + + asset.into_bytes() + } else { + asset_response.into_owned() + }; + let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path); + Ok(Asset { + bytes: final_data, + mime_type, + csp_header, + }) } pub(crate) fn listeners(&self) -> &Listeners { @@ -747,6 +749,8 @@ mod test { PluginStore::default(), Box::new(|_| false), None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, Default::default(), StateManager::new(), Default::default(), diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs index de6e6675dd49..4645f68e9be3 100644 --- a/crates/tauri/src/manager/webview.rs +++ b/crates/tauri/src/manager/webview.rs @@ -6,7 +6,6 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, fmt, - fs::create_dir_all, sync::{Arc, Mutex, MutexGuard}, }; @@ -28,6 +27,9 @@ use crate::{ webview::PageLoadPayload, }; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::app::OnWebContentProcessTerminate; + use super::{ window::{DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT, DragDropPayload}, {AppManager, EmitPayload}, @@ -37,7 +39,7 @@ use super::{ // and we do not get a secure context without the custom protocol that proxies to the dev server // additionally, we need the custom protocol to inject the initialization scripts on Android // must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol -pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, any(mobile, feature = "cef"))); +pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, any(mobile))); pub(crate) const PROCESS_IPC_MESSAGE_FN: &str = include_str!("../../scripts/process-ipc-message-fn.js"); @@ -70,6 +72,9 @@ pub struct WebviewManager { pub invoke_handler: Box>, /// The page load hook, invoked when the webview performs a navigation. pub on_page_load: Option>>, + /// The web content process termination hook. + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub on_web_content_process_terminate: Option>>, /// The webview protocols available to all webviews. pub uri_scheme_protocols: Mutex>>>, /// Webview event listeners to all webviews. @@ -299,6 +304,28 @@ impl WebviewManager { } })); + #[cfg(any(target_os = "macos", target_os = "ios"))] + if pending.on_web_content_process_terminate_handler.is_none() { + let app_manager_ = manager.manager_owned(); + if app_manager_ + .webview + .on_web_content_process_terminate + .is_some() + { + let label_ = pending.label.clone(); + pending + .on_web_content_process_terminate_handler + .replace(Box::new(move || { + if let Some(w) = app_manager_.get_webview(&label_) + && let Some(on_web_content_process_terminate) = + &app_manager_.webview.on_web_content_process_terminate + { + on_web_content_process_terminate(&w); + } + })); + } + } + #[cfg(feature = "protocol-asset")] if !registered_scheme_protocols.contains(&"asset".into()) { let asset_scope = app_manager @@ -474,8 +501,8 @@ impl WebviewManager { let html = String::from_utf8_lossy(&body).into_owned(); // naive way to check if it's an html if html.contains('<') && html.contains('>') { - let document = tauri_utils::html::parse(html); - tauri_utils::html::inject_csp(&document, &csp.to_string()); + let document = tauri_utils::html2::parse(html); + tauri_utils::html2::inject_csp(&document, &csp.to_string()); url.set_path(&format!("{},{document}", mime::TEXT_HTML)); } } @@ -527,13 +554,6 @@ impl WebviewManager { } } - // make sure the directory is created and available to prevent a panic - if let Some(user_data_dir) = &pending.webview_attributes.data_directory - && !user_data_dir.exists() - { - create_dir_all(user_data_dir)?; - } - #[cfg(all(desktop, not(target_os = "windows")))] if pending.webview_attributes.zoom_hotkeys_enabled { #[derive(Template)] diff --git a/crates/tauri/src/manager/window.rs b/crates/tauri/src/manager/window.rs index 0db48529f8b2..f91bc42d0262 100644 --- a/crates/tauri/src/manager/window.rs +++ b/crates/tauri/src/manager/window.rs @@ -37,6 +37,10 @@ pub(crate) const DRAG_ENTER_EVENT: EventName<&str> = EventName::from_str("tauri: pub(crate) const DRAG_OVER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-over"); pub(crate) const DRAG_DROP_EVENT: EventName<&str> = EventName::from_str("tauri://drag-drop"); pub(crate) const DRAG_LEAVE_EVENT: EventName<&str> = EventName::from_str("tauri://drag-leave"); +#[cfg(mobile)] +pub(crate) const WINDOW_SUSPENDED_EVENT: EventName<&str> = EventName::from_str("tauri://suspended"); +#[cfg(mobile)] +pub(crate) const WINDOW_RESUMED_EVENT: EventName<&str> = EventName::from_str("tauri://resumed"); pub struct WindowManager { pub windows: Mutex>>, @@ -265,6 +269,10 @@ fn on_window_event(window: &Window, event: &WindowEvent) -> crate _ => unimplemented!(), }, WindowEvent::ThemeChanged(theme) => window.emit_to_window(WINDOW_THEME_CHANGED, &theme)?, + #[cfg(mobile)] + WindowEvent::Suspended => window.emit_to_window(WINDOW_SUSPENDED_EVENT, &())?, + #[cfg(mobile)] + WindowEvent::Resumed => window.emit_to_window(WINDOW_RESUMED_EVENT, &())?, } Ok(()) } diff --git a/crates/tauri/src/menu/builders/menu.rs b/crates/tauri/src/menu/builders/menu.rs index 3da31f52a746..61819abf2a42 100644 --- a/crates/tauri/src/menu/builders/menu.rs +++ b/crates/tauri/src/menu/builders/menu.rs @@ -642,6 +642,31 @@ macro_rules! shared_menu_builder { .push(PredefinedMenuItem::services(self.manager, Some(text.as_ref())).map(|i| i.kind())); self } + + /// Add Bring All to Front menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::bring_all_to_front(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Bring All to Front menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::bring_all_to_front(self.manager, Some(text.as_ref())) + .map(|i| i.kind()), + ); + self + } } }; } diff --git a/crates/tauri/src/menu/plugin.rs b/crates/tauri/src/menu/plugin.rs index 308421531e88..68f96c14c387 100644 --- a/crates/tauri/src/menu/plugin.rs +++ b/crates/tauri/src/menu/plugin.rs @@ -90,6 +90,7 @@ enum Predefined { Quit, About(Option), Services, + BringAllToFront, } #[derive(Deserialize)] @@ -302,6 +303,9 @@ impl PredefinedMenuItemPayload { PredefinedMenuItem::about(webview, self.text.as_deref(), metadata) } Predefined::Services => PredefinedMenuItem::services(webview, self.text.as_deref()), + Predefined::BringAllToFront => { + PredefinedMenuItem::bring_all_to_front(webview, self.text.as_deref()) + } } } } diff --git a/crates/tauri/src/menu/predefined.rs b/crates/tauri/src/menu/predefined.rs index 1023f8162470..cfb4b2a8296e 100644 --- a/crates/tauri/src/menu/predefined.rs +++ b/crates/tauri/src/menu/predefined.rs @@ -384,6 +384,29 @@ impl PredefinedMenuItem { Ok(Self(Arc::new(item))) } + /// Bring All to Front menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::bring_all_to_front(text.as_deref()); + PredefinedMenuItemInner { + id: item.id().clone(), + inner: Some(item), + app_handle, + } + })?; + + Ok(Self(Arc::new(item))) + } + /// Returns a unique identifier associated with this menu item. pub fn id(&self) -> &MenuId { &self.0.id diff --git a/crates/tauri/src/path/mod.rs b/crates/tauri/src/path/mod.rs index 7e57e8795e97..4188611c7ce6 100644 --- a/crates/tauri/src/path/mod.rs +++ b/crates/tauri/src/path/mod.rs @@ -73,6 +73,12 @@ impl FromStr for SafePathBuf { } } +impl From for PathBuf { + fn from(path: SafePathBuf) -> Self { + path.0 + } +} + impl<'de> Deserialize<'de> for SafePathBuf { fn deserialize(deserializer: D) -> std::result::Result where diff --git a/crates/tauri/src/plugin/mobile.rs b/crates/tauri/src/plugin/mobile.rs index eb09b2e0fe59..253f85d4df93 100644 --- a/crates/tauri/src/plugin/mobile.rs +++ b/crates/tauri/src/plugin/mobile.rs @@ -373,12 +373,12 @@ pub(crate) fn run_command, F: FnOnce(PluginResponse) + CStr::from_ptr(payload) }; - if let Some(handler) = PENDING_PLUGIN_CALLS + let handler = PENDING_PLUGIN_CALLS .get_or_init(Default::default) .lock() .unwrap() - .remove(&id) - { + .remove(&id); + if let Some(handler) = handler { let json = payload.to_str().unwrap(); match serde_json::from_str(json) { Ok(payload) => { diff --git a/crates/tauri/src/protocol/asset.rs b/crates/tauri/src/protocol/asset.rs index 86334ffc08c9..7403428cecd7 100644 --- a/crates/tauri/src/protocol/asset.rs +++ b/crates/tauri/src/protocol/asset.rs @@ -5,10 +5,10 @@ use crate::{path::SafePathBuf, scope, webview::UriSchemeProtocolHandler}; use http::{Request, Response, header::*, status::StatusCode}; use http_range::HttpRange; +use std::fs::File; +use std::io::{Read, Seek, Write}; use std::{borrow::Cow, io::SeekFrom}; use tauri_utils::mime_type::MimeType; -use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; pub fn get(scope: scope::fs::Scope, window_origin: String) -> UriSchemeProtocolHandler { Box::new( @@ -49,7 +49,7 @@ fn get_response( } // Separate block for easier error handling - let mut file = match crate::async_runtime::safe_block_on(File::open(path.clone())) { + let mut file = match File::open(path.clone()) { Ok(file) => file, Err(e) => { #[cfg(target_os = "android")] @@ -74,32 +74,20 @@ fn get_response( } }; - let (mut file, len, mime_type, read_bytes) = crate::async_runtime::safe_block_on(async move { - // get file length - let len = { - let old_pos = file.stream_position().await?; - let len = file.seek(SeekFrom::End(0)).await?; - file.seek(SeekFrom::Start(old_pos)).await?; - len - }; - + let len = file.metadata()?.len(); + let (mime_type, read_bytes) = { // get file mime type - let (mime_type, read_bytes) = { - let nbytes = len.min(8192); - let mut magic_buf = Vec::with_capacity(nbytes as usize); - let old_pos = file.stream_position().await?; - (&mut file).take(nbytes).read_to_end(&mut magic_buf).await?; - file.seek(SeekFrom::Start(old_pos)).await?; - ( - MimeType::parse(&magic_buf, &path), - // return the `magic_bytes` if we read the whole file - // to avoid reading it again later if this is not a range request - if len < 8192 { Some(magic_buf) } else { None }, - ) - }; - - Ok::<(File, u64, String, Option>), anyhow::Error>((file, len, mime_type, read_bytes)) - })?; + let nbytes = len.min(8192); + let mut magic_buf = Vec::with_capacity(nbytes as usize); + (&mut file).take(nbytes).read_to_end(&mut magic_buf)?; + file.rewind()?; + ( + MimeType::parse(&magic_buf, &path), + // return the `magic_bytes` if we read the whole file + // to avoid reading it again later if this is not a range request + if len < 8192 { Some(magic_buf) } else { None }, + ) + }; resp = resp.header(CONTENT_TYPE, &mime_type); @@ -152,12 +140,12 @@ fn get_response( // calculate number of bytes needed to be read let nbytes = end + 1 - start; - let buf = crate::async_runtime::safe_block_on(async move { + let buf = { let mut buf = Vec::with_capacity(nbytes as usize); - file.seek(SeekFrom::Start(start)).await?; - file.take(nbytes).read_to_end(&mut buf).await?; - Ok::, anyhow::Error>(buf) - })?; + file.seek(SeekFrom::Start(start))?; + file.take(nbytes).read_to_end(&mut buf)?; + buf + }; resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); resp = resp.header(CONTENT_LENGTH, end + 1 - start); @@ -190,38 +178,34 @@ fn get_response( format!("multipart/byteranges; boundary={boundary}"), ); - let buf = crate::async_runtime::safe_block_on(async move { + let buf = { // multi-part range header let mut buf = Vec::new(); for (start, end) in ranges { // a new range is being written, write the range boundary - buf.write_all(boundary_sep.as_bytes()).await?; + buf.write_all(boundary_sep.as_bytes())?; // write the needed headers `Content-Type` and `Content-Range` - buf - .write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes()) - .await?; - buf - .write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes()) - .await?; + buf.write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes())?; + buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; // write the separator to indicate the start of the range body - buf.write_all("\r\n".as_bytes()).await?; + buf.write_all("\r\n".as_bytes())?; // calculate number of bytes needed to be read let nbytes = end + 1 - start; let mut local_buf = Vec::with_capacity(nbytes as usize); - file.seek(SeekFrom::Start(start)).await?; - (&mut file).take(nbytes).read_to_end(&mut local_buf).await?; + file.seek(SeekFrom::Start(start))?; + (&mut file).take(nbytes).read_to_end(&mut local_buf)?; buf.extend_from_slice(&local_buf); } // all ranges have been written, write the closing boundary - buf.write_all(boundary_closer.as_bytes()).await?; + buf.write_all(boundary_closer.as_bytes())?; - Ok::, anyhow::Error>(buf) - })?; + buf + }; resp.body(buf.into()) } } else if request.method() == http::Method::HEAD { @@ -234,11 +218,9 @@ fn get_response( let buf = if let Some(b) = read_bytes { b } else { - crate::async_runtime::safe_block_on(async move { - let mut local_buf = Vec::with_capacity(len as usize); - file.read_to_end(&mut local_buf).await?; - Ok::, anyhow::Error>(local_buf) - })? + let mut local_buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut local_buf)?; + local_buf }; resp = resp.header(CONTENT_LENGTH, len); resp.body(buf.into()) diff --git a/crates/tauri/src/resources/mod.rs b/crates/tauri/src/resources/mod.rs index 7515d46fd833..9561558585ff 100644 --- a/crates/tauri/src/resources/mod.rs +++ b/crates/tauri/src/resources/mod.rs @@ -140,7 +140,7 @@ impl ResourceTable { } /// Returns a reference counted pointer to the resource of the given `rid`. - /// If `rid` is not present, this function returns [`Error::BadResourceId`]. + /// If `rid` is not present, this function returns [`crate::Error::BadResourceId`]. pub fn get_any(&self, rid: ResourceId) -> crate::Result> { self .index diff --git a/crates/tauri/src/scope/fs.rs b/crates/tauri/src/scope/fs.rs index f22b142b17e2..cbeb735a4816 100644 --- a/crates/tauri/src/scope/fs.rs +++ b/crates/tauri/src/scope/fs.rs @@ -77,7 +77,7 @@ fn push_pattern, F: Fn(&str) -> Result crate::Result<()> { - // Reconstruct pattern path components with appropraite separator + // Reconstruct pattern path components with appropriate separator // so `some\path/to/dir/**\*` would be `some/path/to/dir/**/*` on Unix // and `some\path\to\dir\**\*` on Windows. let path: PathBuf = pattern.as_ref().components().collect(); diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs index 12aae4291d3a..032f80efcd2e 100644 --- a/crates/tauri/src/test/mock_runtime.rs +++ b/crates/tauri/src/test/mock_runtime.rs @@ -538,6 +538,21 @@ impl WindowBuilder for MockWindowBuilder { fn background_color(self, _color: tauri_utils::config::Color) -> Self { self } + + #[cfg(target_os = "android")] + fn activity_name>(self, _class_name: S) -> Self { + self + } + + #[cfg(target_os = "android")] + fn created_by_activity_name>(self, _class_name: S) -> Self { + self + } + + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(self, _identifier: S) -> Self { + self + } } impl WebviewDispatch for MockWebviewDispatcher { @@ -582,6 +597,19 @@ impl WebviewDispatch for MockWebviewDispatcher { Ok(()) } + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self + .last_evaluated_script + .lock() + .unwrap() + .replace(script.into()); + Ok(()) + } + fn url(&self) -> Result { Ok(self.url.lock().unwrap().clone()) } @@ -816,6 +844,16 @@ impl WindowDispatch for MockWindowDispatcher { unimplemented!() } + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result { + unimplemented!() + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result { + unimplemented!() + } + fn window_handle( &self, ) -> std::result::Result, raw_window_handle::HandleError> { diff --git a/crates/tauri/src/test/mod.rs b/crates/tauri/src/test/mod.rs index 41d843a34422..c8d31d8b05c7 100644 --- a/crates/tauri/src/test/mod.rs +++ b/crates/tauri/src/test/mod.rs @@ -216,7 +216,11 @@ pub fn mock_app() -> App { /// cmd: "ping".into(), /// callback: tauri::ipc::CallbackFn(0), /// error: tauri::ipc::CallbackFn(1), -/// url: "http://tauri.localhost".parse().unwrap(), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), /// body: tauri::ipc::InvokeBody::default(), /// headers: Default::default(), /// invoke_key: tauri::test::INVOKE_KEY.to_string(), @@ -275,7 +279,11 @@ pub fn assert_ipc_response< /// cmd: "ping".into(), /// callback: tauri::ipc::CallbackFn(0), /// error: tauri::ipc::CallbackFn(1), -/// url: "http://tauri.localhost".parse().unwrap(), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), /// body: tauri::ipc::InvokeBody::default(), /// headers: Default::default(), /// invoke_key: tauri::test::INVOKE_KEY.to_string(), diff --git a/crates/tauri/src/tray/mod.rs b/crates/tauri/src/tray/mod.rs index 67a33e53aa83..f459200870a4 100644 --- a/crates/tauri/src/tray/mod.rs +++ b/crates/tauri/src/tray/mod.rs @@ -566,6 +566,38 @@ impl TrayIcon { Ok(()) } + /// Sets the tray icon and template status atomically. **macOS only**. + /// + /// On macOS, calling `set_icon` followed by `set_icon_as_template` causes a visible + /// flicker as the icon is rendered twice. This method sets both atomically to prevent that. + /// + /// ## Platform-specific: + /// + /// - **Linux / Windows:** Falls back to calling `set_icon`. + pub fn set_icon_with_as_template( + &self, + icon: Option>, + #[allow(unused)] is_template: bool, + ) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let tray_icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| { + self_ + .inner + .set_icon_with_as_template(tray_icon, is_template) + })??; + } + #[cfg(not(target_os = "macos"))] + { + self.set_icon(icon)?; + } + Ok(()) + } + /// Disable or enable showing the tray menu on left click. /// /// diff --git a/crates/tauri/src/tray/plugin.rs b/crates/tauri/src/tray/plugin.rs index 0d0483a5e4ba..1aa75d700467 100644 --- a/crates/tauri/src/tray/plugin.rs +++ b/crates/tauri/src/tray/plugin.rs @@ -202,6 +202,24 @@ fn set_icon_as_template( tray.set_icon_as_template(as_template) } +#[command(root = "crate")] +fn set_icon_with_as_template( + app: AppHandle, + webview: Webview, + rid: ResourceId, + icon: Option, + as_template: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + let webview_resources_table = webview.resources_table(); + let icon = match icon { + Some(i) => Some(i.into_img(&webview_resources_table)?.as_ref().clone()), + None => None, + }; + tray.set_icon_with_as_template(icon, as_template) +} + #[command(root = "crate")] fn set_show_menu_on_left_click( app: AppHandle, @@ -227,6 +245,7 @@ pub(crate) fn init() -> TauriPlugin { set_visible, set_temp_dir_path, set_icon_as_template, + set_icon_with_as_template, set_show_menu_on_left_click, ]) .build() diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index 89ed4ab29f47..dffc7a3fc477 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -81,6 +81,14 @@ pub(crate) struct CreatedEvent { pub(crate) label: String, } +fn is_url_for_custom_protocol(current_url: &Url, protocol: &str, protocol_url: &Url) -> bool { + if protocol_url.scheme() == protocol { + current_url.scheme() == protocol + } else { + current_url.scheme() == protocol_url.scheme() && current_url.domain() == protocol_url.domain() + } +} + /// Download event for the [`WebviewBuilder#method.on_download`] hook. #[non_exhaustive] pub enum DownloadEvent<'a> { @@ -254,8 +262,8 @@ pub enum NewWindowResponse { /// /// ## Platform-specific: /// - /// **Linux**: The webview must be related to the caller webview. See [`WebviewBuilder::related_view`]. - /// **Windows**: The webview must use the same environment as the caller webview. See [`WebviewBuilder::environment`]. + /// **Linux**: The webview must be related to the caller webview. See [`WebviewBuilder::with_related_view`]. + /// **Windows**: The webview must use the same environment as the caller webview. See [`WebviewBuilder::with_environment`]. /// **macOS**: The webview must use the same webview configuration as the caller webview. See [`WebviewBuilder::with_webview_configuration`] and [`NewWindowFeatures::webview_configuration`]. Create { /// Window that was created. @@ -623,7 +631,6 @@ tauri::Builder::::new() /// # Platform-specific /// /// - **Android / iOS**: Not supported. - /// - **Windows**: The closure is executed on a separate thread to prevent a deadlock. /// /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open pub fn on_new_window< @@ -1231,7 +1238,7 @@ fn main() { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[must_use] pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { self.webview_attributes.background_throttling = Some(policy); @@ -1264,6 +1271,29 @@ fn main() { self } + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes = self.webview_attributes.general_autofill_enabled(enabled); + self + } + /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only. /// /// Default is true. @@ -1740,7 +1770,7 @@ tauri::Builder::::new() "#### )] #[cfg(feature = "wry")] - #[cfg_attr(docsrs, doc(feature = "wry"))] + #[cfg_attr(docsrs, doc(cfg(feature = "wry")))] pub fn with_webview( &self, f: F, @@ -1810,8 +1840,13 @@ tauri::Builder::::new() // or from a custom protocol registered by the user || ({ - let protocol_urls = self.manager().webview.uri_scheme_protocols.lock().unwrap().keys().map(|url| Url::parse(&R::custom_scheme_url(url, uses_https)).unwrap()).collect::>(); - protocol_urls.iter().any(|url| url.scheme() == current_url.scheme() && url.domain() == current_url.domain()) + let protocols = self.manager().webview.uri_scheme_protocols.lock().unwrap(); + + protocols.keys().any(|protocol| { + let protocol_url = Url::parse(&R::custom_scheme_url(protocol, uses_https)).unwrap(); + + is_url_for_custom_protocol(current_url, protocol, &protocol_url) + }) }) } @@ -1893,8 +1928,11 @@ tauri::Builder::::new() (plugin, command) }); - // we only check ACL on plugin commands or if the app defined its ACL manifest - if (plugin_command.is_some() || has_app_acl_manifest) + // Check ACL on plugin commands, when the app defined its ACL manifest, + // or when the request comes from a non-local (remote) origin. This + // ensures remote content can never reach custom commands unless an + // explicit `remote` capability has been configured for them. + if (plugin_command.is_some() || has_app_acl_manifest || !is_local) // TODO: Remove this special check in v3 && request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND && invoke.acl.is_none() @@ -1996,6 +2034,22 @@ tauri::Builder::::new() .map_err(Into::into) } + /// Evaluate JavaScript with callback function on this webview. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + pub fn eval_with_callback( + &self, + js: impl Into, + callback: impl Fn(String) + Send + 'static, + ) -> crate::Result<()> { + self + .webview + .dispatcher + .eval_script_with_callback(js.into(), callback) + .map_err(Into::into) + } + /// Register a JS event listener and return its identifier. pub(crate) fn listen_js( &self, @@ -2442,6 +2496,16 @@ impl ManagerBase for Webview { fn managed_app_handle(&self) -> &AppHandle { &self.app_handle } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.window().activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.window().scene_identifier()) + } } impl<'de, R: Runtime> CommandArg<'de, R> for Webview { @@ -2471,25 +2535,142 @@ impl ResolvedScope { #[cfg(test)] mod tests { + use url::Url; + + fn test_webview_window() -> crate::WebviewWindow { + use crate::test::{mock_builder, mock_context, noop_assets}; + + // Create a mock app with proper context + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + + // Create a webview window + crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + } + #[test] fn webview_is_send_sync() { crate::test_utils::assert_send::(); crate::test_utils::assert_sync::(); } - #[cfg(target_os = "macos")] #[test] - fn test_webview_window_has_set_simple_fullscreen_method() { + fn tauri_protocol_is_local() { + let webview = test_webview_window().webview; + + assert!(webview.is_local_url(&Url::parse("tauri://localhost/").unwrap())); + } + + #[test] + fn direct_custom_protocol_is_local() { use crate::test::{mock_builder, mock_context, noop_assets}; - // Create a mock app with proper context + let app = mock_builder() + .register_uri_scheme_protocol("myproto", |_, _| { + http::Response::builder().body(Vec::new()).unwrap() + }) + .build(mock_context(noop_assets())) + .unwrap(); + let webview = crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + .webview; + + let url = |s| Url::parse(s).unwrap(); + + assert!(webview.is_local_url(&url("myproto://localhost/"))); + assert!(!webview.is_local_url(&url("https://myproto.localhost/"))); + } + + #[test] + fn http_custom_protocol_rejects_spoofed_domain() { + let protocol_url = Url::parse("https://myproto.localhost/").unwrap(); + let url = |s| Url::parse(s).unwrap(); + + assert!(super::is_url_for_custom_protocol( + &url("https://myproto.localhost/"), + "myproto", + &protocol_url + )); + + // Attacker domain that starts with a registered protocol name must not be local. + assert!(!super::is_url_for_custom_protocol( + &url("https://myproto.evil.com/"), + "myproto", + &protocol_url + )); + assert!(!super::is_url_for_custom_protocol( + &url("https://notregistered.localhost/"), + "myproto", + &protocol_url + )); + } + + /// Custom (non-plugin) commands must be rejected when the IPC request + /// originates from a remote URL, even when no `AppManifest` has been + /// configured. Only local (bundled) origins should be able to reach + /// custom commands. + #[test] + fn remote_origin_blocked_for_custom_commands_without_app_manifest() { + use crate::test::{INVOKE_KEY, mock_builder, mock_context, noop_assets}; + use crate::webview::InvokeRequest; + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); - // Get or create a webview window - let webview_window = - crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) - .build() - .unwrap(); + let webview = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + // Request from a remote origin for a custom (non-plugin) command + // - should be rejected even without an AppManifest. + let remote_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "https://evil.com".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + assert!( + remote_result.is_err(), + "custom command should be rejected from a remote origin" + ); + + // Same command from the local origin - should NOT be rejected by the + // remote-origin guard (it may still fail because the command doesn't + // exist, but the error message will be different). + let local_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "tauri://localhost".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + // The local request should either succeed or fail for a reason OTHER + // than "not allowed from remote context". + if let Err(e) = &local_result { + let msg = e.to_string(); + assert!( + !msg.contains("not allowed from remote context"), + "local origin should not be blocked by the remote-origin guard, got: {msg}" + ); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn test_webview_window_has_set_simple_fullscreen_method() { + let webview_window = test_webview_window(); // This should compile if set_simple_fullscreen exists let result = webview_window.set_simple_fullscreen(true); diff --git a/crates/tauri/src/webview/plugin.rs b/crates/tauri/src/webview/plugin.rs index 8f48d1dc3f9a..fb37b5c22a7e 100644 --- a/crates/tauri/src/webview/plugin.rs +++ b/crates/tauri/src/webview/plugin.rs @@ -5,86 +5,49 @@ //! The tauri plugin to create and manipulate windows from JS. use crate::{ - Runtime, + AppHandle, Runtime, WebviewWindowBuilder, command, plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, + utils::config::WindowConfig, }; -#[cfg(desktop)] -mod desktop_commands { +#[derive(serde::Serialize)] +struct WebviewRef { + window_label: String, + label: String, +} - use serde::Serialize; - use tauri_runtime::dpi::{Position, Size}; - use tauri_utils::config::WindowConfig; +#[command(root = "crate")] +async fn get_all_webviews(app: AppHandle) -> Vec { + app + .manager() + .webviews() + .values() + .map(|webview| WebviewRef { + window_label: webview.window_ref().label().into(), + label: webview.label().into(), + }) + .collect() +} + +#[command(root = "crate")] +async fn create_webview_window( + app: AppHandle, + options: WindowConfig, +) -> crate::Result<()> { + WebviewWindowBuilder::from_config(&app, &options)?.build()?; + Ok(()) +} +#[cfg(desktop)] +mod desktop_commands { use super::*; use crate::{ - AppHandle, Webview, WebviewWindowBuilder, command, sealed::ManagerBase, webview::Color, + Webview, command, + runtime::dpi::{Position, Size}, + utils::config::Color, }; - #[derive(Serialize)] - pub struct WebviewRef { - window_label: String, - label: String, - } - - #[command(root = "crate")] - pub async fn get_all_webviews(app: AppHandle) -> Vec { - app - .manager() - .webviews() - .values() - .map(|webview| WebviewRef { - window_label: webview.window_ref().label().into(), - label: webview.label().into(), - }) - .collect() - } - - #[command(root = "crate")] - pub async fn create_webview_window( - app: AppHandle, - options: WindowConfig, - ) -> crate::Result<()> { - WebviewWindowBuilder::from_config(&app, &options)?.build()?; - Ok(()) - } - - #[cfg(not(feature = "unstable"))] - #[command(root = "crate")] - pub async fn create_webview() -> crate::Result<()> { - Err(crate::Error::UnstableFeatureNotSupported) - } - - #[cfg(feature = "unstable")] - #[command(root = "crate")] - pub async fn create_webview( - app: AppHandle, - window_label: String, - options: WindowConfig, - ) -> crate::Result<()> { - use anyhow::Context; - - let window = app - .manager() - .get_window(&window_label) - .ok_or(crate::Error::WindowNotFound)?; - - let x = options.x.context("missing parameter `options.x`")?; - let y = options.y.context("missing parameter `options.y`")?; - let width = options.width; - let height = options.height; - - let builder = crate::webview::WebviewBuilder::from_config(&options); - - window.add_child( - builder, - tauri_runtime::dpi::LogicalPosition::new(x, y), - tauri_runtime::dpi::LogicalSize::new(width, height), - )?; - - Ok(()) - } - fn get_webview( webview: Webview, label: Option, @@ -163,6 +126,42 @@ mod desktop_commands { ); setter!(clear_all_browsing_data, clear_all_browsing_data); + #[cfg(not(feature = "unstable"))] + #[command(root = "crate")] + pub async fn create_webview() -> crate::Result<()> { + Err(crate::Error::UnstableFeatureNotSupported) + } + + #[cfg(feature = "unstable")] + #[command(root = "crate")] + pub async fn create_webview( + app: crate::AppHandle, + window_label: String, + options: WindowConfig, + ) -> crate::Result<()> { + use anyhow::Context; + + let window = app + .manager() + .get_window(&window_label) + .ok_or(crate::Error::WindowNotFound)?; + + let x = options.x.context("missing parameter `options.x`")?; + let y = options.y.context("missing parameter `options.y`")?; + let width = options.width; + let height = options.height; + + let builder = crate::webview::WebviewBuilder::from_config(&options); + + window.add_child( + builder, + tauri_runtime::dpi::LogicalPosition::new(x, y), + tauri_runtime::dpi::LogicalSize::new(width, height), + )?; + + Ok(()) + } + #[command(root = "crate")] pub async fn reparent( webview: crate::Webview, @@ -231,39 +230,29 @@ pub fn init() -> TauriPlugin { } builder - .invoke_handler( - #[cfg(desktop)] - crate::generate_handler![ - #![plugin(webview)] - desktop_commands::create_webview, - desktop_commands::create_webview_window, - // getters - desktop_commands::get_all_webviews, - desktop_commands::webview_position, - desktop_commands::webview_size, - // setters - desktop_commands::webview_close, - desktop_commands::set_webview_size, - desktop_commands::set_webview_position, - desktop_commands::set_webview_focus, - desktop_commands::set_webview_auto_resize, - desktop_commands::set_webview_background_color, - desktop_commands::set_webview_zoom, - desktop_commands::webview_hide, - desktop_commands::webview_show, - desktop_commands::print, - desktop_commands::reparent, - desktop_commands::clear_all_browsing_data, - #[cfg(any(debug_assertions, feature = "devtools"))] - desktop_commands::internal_toggle_devtools, - ], - #[cfg(mobile)] - |invoke| { - invoke - .resolver - .reject("Webview API not available on mobile"); - true - }, - ) + .invoke_handler(crate::generate_handler![ + #![plugin(webview)] + create_webview_window, + get_all_webviews, + #[cfg(desktop)] desktop_commands::create_webview, + // getters + #[cfg(desktop)] desktop_commands::webview_position, + #[cfg(desktop)] desktop_commands::webview_size, + // setters + #[cfg(desktop)] desktop_commands::webview_close, + #[cfg(desktop)] desktop_commands::set_webview_size, + #[cfg(desktop)] desktop_commands::set_webview_position, + #[cfg(desktop)] desktop_commands::set_webview_focus, + #[cfg(desktop)] desktop_commands::set_webview_auto_resize, + #[cfg(desktop)] desktop_commands::set_webview_background_color, + #[cfg(desktop)] desktop_commands::set_webview_zoom, + #[cfg(desktop)] desktop_commands::webview_hide, + #[cfg(desktop)] desktop_commands::webview_show, + #[cfg(desktop)] desktop_commands::print, + #[cfg(desktop)] desktop_commands::clear_all_browsing_data, + #[cfg(desktop)] desktop_commands::reparent, + #[cfg(all(desktop, any(debug_assertions, feature = "devtools")))] + desktop_commands::internal_toggle_devtools, + ]) .build() } diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index cfa88a89dea7..c3bf51883d17 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -14,7 +14,7 @@ use crate::{ Emitter, EventName, Listener, ResourceTable, Window, event::EventTarget, ipc::ScopeObject, - runtime::dpi::{PhysicalPosition, PhysicalSize}, + runtime::dpi::{PhysicalPosition, PhysicalSize, Position, Size}, webview::{NewWindowResponse, ScrollBarStyle}, window::Monitor, }; @@ -22,11 +22,7 @@ use crate::{ use crate::{ image::Image, menu::{ContextMenu, Menu}, - runtime::{ - UserAttentionType, - dpi::{Position, Size}, - window::CursorIcon, - }, + runtime::{UserAttentionType, window::CursorIcon}, }; use tauri_runtime::webview::NewWindowFeatures; use tauri_utils::config::{BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig}; @@ -352,7 +348,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { /// # Platform-specific /// /// - **Android / iOS**: Not supported. - /// - **Windows**: The closure is executed on a separate thread to prevent a deadlock. /// /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open pub fn on_new_window< @@ -506,44 +501,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// The initial position of the window in logical pixels. - #[must_use] - pub fn position(mut self, x: f64, y: f64) -> Self { - self.window_builder = self.window_builder.position(x, y); - self - } - - /// Window size in logical pixels. - #[must_use] - pub fn inner_size(mut self, width: f64, height: f64) -> Self { - self.window_builder = self.window_builder.inner_size(width, height); - self - } - - /// Window min inner size in logical pixels. - #[must_use] - pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { - self.window_builder = self.window_builder.min_inner_size(min_width, min_height); - self - } - - /// Window max inner size in logical pixels. - #[must_use] - pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { - self.window_builder = self.window_builder.max_inner_size(max_width, max_height); - self - } - - /// Window inner size constraints. - #[must_use] - pub fn inner_size_constraints( - mut self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> Self { - self.window_builder = self.window_builder.inner_size_constraints(constraints); - self - } - /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) /// on creation, which means the window size will be limited to `monitor size - taskbar size` /// @@ -572,14 +529,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Whether the window is resizable or not. - /// When resizable is set to false, native window's maximize button is automatically disabled. - #[must_use] - pub fn resizable(mut self, resizable: bool) -> Self { - self.window_builder = self.window_builder.resizable(resizable); - self - } - /// Whether the window's native maximize button is enabled or not. /// If resizable is set to false, this setting is ignored. /// @@ -617,13 +566,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// The title of the window in the title bar. - #[must_use] - pub fn title>(mut self, title: S) -> Self { - self.window_builder = self.window_builder.title(title); - self - } - /// Whether to start the window in fullscreen or not. #[must_use] pub fn fullscreen(mut self, fullscreen: bool) -> Self { @@ -631,25 +573,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Sets the window to be initially focused. - #[must_use] - #[deprecated( - since = "1.2.0", - note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." - )] - pub fn focus(mut self) -> Self { - self.window_builder = self.window_builder.focused(true); - self.webview_builder = self.webview_builder.focused(true); - self - } - - /// Whether the window will be focusable or not. - #[must_use] - pub fn focusable(mut self, focusable: bool) -> Self { - self.window_builder = self.window_builder.focusable(focusable); - self - } - /// Whether the window will be initially focused or not. #[must_use] pub fn focused(mut self, focused: bool) -> Self { @@ -665,24 +588,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Whether the window should be immediately visible upon creation. - #[must_use] - pub fn visible(mut self, visible: bool) -> Self { - self.window_builder = self.window_builder.visible(visible); - self - } - - /// Forces a theme or uses the system settings if None was provided. - /// - /// ## Platform-specific - /// - /// - **macOS**: Only supported on macOS 10.14+. - #[must_use] - pub fn theme(mut self, theme: Option) -> Self { - self.window_builder = self.window_builder.theme(theme); - self - } - /// Whether the window should have borders and bars. #[must_use] pub fn decorations(mut self, decorations: bool) -> Self { @@ -713,13 +618,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Prevents the window contents from being captured by other apps. - #[must_use] - pub fn content_protected(mut self, protected: bool) -> Self { - self.window_builder = self.window_builder.content_protected(protected); - self - } - /// Sets the window icon. pub fn icon(mut self, icon: Image<'a>) -> crate::Result { self.window_builder = self.window_builder.icon(icon)?; @@ -941,6 +839,106 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { } } +/// Window APIs. +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// The initial position of the window in logical pixels. + #[must_use] + pub fn position(mut self, x: f64, y: f64) -> Self { + self.window_builder = self.window_builder.position(x, y); + self + } + + /// Window size in logical pixels. + #[must_use] + pub fn inner_size(mut self, width: f64, height: f64) -> Self { + self.window_builder = self.window_builder.inner_size(width, height); + self + } + + /// Window min inner size in logical pixels. + #[must_use] + pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.window_builder = self.window_builder.min_inner_size(min_width, min_height); + self + } + + /// Window max inner size in logical pixels. + #[must_use] + pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.window_builder = self.window_builder.max_inner_size(max_width, max_height); + self + } + + /// Window inner size constraints. + #[must_use] + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self.window_builder = self.window_builder.inner_size_constraints(constraints); + self + } + + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + pub fn resizable(mut self, resizable: bool) -> Self { + self.window_builder = self.window_builder.resizable(resizable); + self + } + + /// The title of the window in the title bar. + #[must_use] + pub fn title>(mut self, title: S) -> Self { + self.window_builder = self.window_builder.title(title); + self + } + + /// Sets the window to be initially focused. + #[must_use] + #[deprecated( + since = "1.2.0", + note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." + )] + pub fn focus(mut self) -> Self { + self.window_builder = self.window_builder.focused(true); + self.webview_builder = self.webview_builder.focused(true); + self + } + + /// Whether the window will be focusable or not. + #[must_use] + pub fn focusable(mut self, focusable: bool) -> Self { + self.window_builder = self.window_builder.focusable(focusable); + self + } + + /// Whether the window should be immediately visible upon creation. + #[must_use] + pub fn visible(mut self, visible: bool) -> Self { + self.window_builder = self.window_builder.visible(visible); + self + } + + /// Forces a theme or uses the system settings if None was provided. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + #[must_use] + pub fn theme(mut self, theme: Option) -> Self { + self.window_builder = self.window_builder.theme(theme); + self + } + + /// Prevents the window contents from being captured by other apps. + #[must_use] + pub fn content_protected(mut self, protected: bool) -> Self { + self.window_builder = self.window_builder.content_protected(protected); + self + } +} + /// Webview attributes. impl> WebviewWindowBuilder<'_, R, M> { /// Sets whether clicking an inactive window also clicks through to the webview. @@ -987,6 +985,9 @@ impl> WebviewWindowBuilder<'_, R, M> { /// }); /// } /// ``` + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) #[must_use] pub fn initialization_script(mut self, script: impl Into) -> Self { self.webview_builder = self.webview_builder.initialization_script(script); @@ -1029,6 +1030,9 @@ impl> WebviewWindowBuilder<'_, R, M> { /// }); /// } /// ``` + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) #[must_use] pub fn initialization_script_for_all_frames(mut self, script: impl Into) -> Self { self.webview_builder = self @@ -1242,7 +1246,7 @@ impl> WebviewWindowBuilder<'_, R, M> { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[must_use] pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { self.webview_builder = self.webview_builder.background_throttling(policy); @@ -1275,6 +1279,29 @@ impl> WebviewWindowBuilder<'_, R, M> { self } + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.general_autofill_enabled(enabled); + self + } + /// Allows overriding the keyboard accessory view on iOS. /// Returning `None` effectively removes the view. /// @@ -1398,6 +1425,40 @@ impl> WebviewWindowBuilder<'_, crate::Wry, M> { } } +// Android specific APIs +#[cfg(target_os = "android")] +impl> WebviewWindowBuilder<'_, R, M> { + /// The name of the activity to create for this webview window. + pub fn activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.activity_name(class_name); + self + } + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + pub fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.created_by_activity_name(class_name); + self + } +} + +/// iOS specific APIs +#[cfg(target_os = "ios")] +impl> WebviewWindowBuilder<'_, R, M> { + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + pub fn requested_by_scene_identifier(mut self, identifier: String) -> Self { + self.window_builder = self + .window_builder + .requested_by_scene_identifier(identifier); + self + } +} + /// A type that wraps a [`Window`] together with a [`Webview`]. #[default_runtime(crate::Wry, wry)] #[derive(Debug)] @@ -1827,6 +1888,12 @@ impl WebviewWindow { self.window.default_vbox() } + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + pub fn activity_name(&self) -> crate::Result { + self.window.activity_name() + } + /// Returns the current window theme. /// /// ## Platform-specific @@ -1879,17 +1946,6 @@ impl WebviewWindow { self.window.request_user_attention(request_type) } - /// Determines if this window should be resizable. - /// When resizable is set to false, native window's maximize button is automatically disabled. - pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { - self.window.set_resizable(resizable) - } - - /// Enable or disable the window. - pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { - self.webview.window().set_enabled(enabled) - } - /// Determines if this window's native maximize button should be enabled. /// If resizable is set to false, this setting is ignored. /// @@ -1921,11 +1977,6 @@ impl WebviewWindow { self.window.set_closable(closable) } - /// Set this window's title. - pub fn set_title(&self, title: &str) -> crate::Result<()> { - self.window.set_title(title) - } - /// Maximizes this window. pub fn maximize(&self) -> crate::Result<()> { self.window.maximize() @@ -1946,26 +1997,6 @@ impl WebviewWindow { self.window.unminimize() } - /// Show this window. - pub fn show(&self) -> crate::Result<()> { - self.window.show() - } - - /// Hide this window. - pub fn hide(&self) -> crate::Result<()> { - self.window.hide() - } - - /// Closes this window. It emits [`crate::RunEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. - pub fn close(&self) -> crate::Result<()> { - self.window.close() - } - - /// Destroys this window. Similar to [`Self::close`] but does not emit any events and force close the window instead. - pub fn destroy(&self) -> crate::Result<()> { - self.window.destroy() - } - /// Determines if this window should be [decorated]. /// /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration @@ -2041,39 +2072,6 @@ impl WebviewWindow { .set_visible_on_all_workspaces(visible_on_all_workspaces) } - /// Prevents the window contents from being captured by other apps. - pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { - self.window.set_content_protected(protected) - } - - /// Resizes this window. - pub fn set_size>(&self, size: S) -> crate::Result<()> { - self.window.set_size(size.into()) - } - - /// Sets this window's minimum inner size. - pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { - self.window.set_min_size(size.map(|s| s.into())) - } - - /// Sets this window's maximum inner size. - pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { - self.window.set_max_size(size.map(|s| s.into())) - } - - /// Sets this window's minimum inner width. - pub fn set_size_constraints( - &self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> crate::Result<()> { - self.window.set_size_constraints(constraints) - } - - /// Sets this window's position. - pub fn set_position>(&self, position: Pos) -> crate::Result<()> { - self.window.set_position(position) - } - /// Determines if this window should be fullscreen. pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { self.window.set_fullscreen(fullscreen) @@ -2093,41 +2091,11 @@ impl WebviewWindow { self.window.set_simple_fullscreen(enable) } - /// Bring the window to front and focus. - pub fn set_focus(&self) -> crate::Result<()> { - self.window.set_focus() - } - - /// Sets whether the window can be focused. - /// - /// ## Platform-specific - /// - /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. - /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. - pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { - self.window.set_focusable(focusable) - } - /// Sets this window' icon. pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { self.window.set_icon(icon) } - /// Sets the window background color. - /// - /// ## Platform-specific: - /// - /// - **iOS / Android:** Unsupported. - /// - **macOS**: Not implemented for the webview layer.. - /// - **Windows**: - /// - alpha channel is ignored for the window layer. - /// - On Windows 7, transparency is not supported and the alpha value will be ignored for the webview layer.. - /// - On Windows 8 and newer: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` for the webview layer. - pub fn set_background_color(&self, color: Option) -> crate::Result<()> { - self.window.set_background_color(color)?; - self.webview.set_background_color(color) - } - /// Whether to hide the window icon from the taskbar or not. /// /// ## Platform-specific @@ -2236,6 +2204,108 @@ impl WebviewWindow { pub fn set_traffic_light_position(&self, position: Position) -> crate::Result<()> { self.window.set_traffic_light_position(position) } +} + +/// Desktop window setters and actions. +impl WebviewWindow { + /// Determines if this window should be resizable. + /// When resizable is set to false, native window's maximize button is automatically disabled. + pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { + self.window.set_resizable(resizable) + } + + /// Enable or disable the window. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + self.webview.window().set_enabled(enabled) + } + + /// Set this window's title. + pub fn set_title(&self, title: &str) -> crate::Result<()> { + self.window.set_title(title) + } + + /// Show this window. + pub fn show(&self) -> crate::Result<()> { + self.window.show() + } + + /// Hide this window. + pub fn hide(&self) -> crate::Result<()> { + self.window.hide() + } + + /// Closes this window. It emits [`crate::WindowEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. + pub fn close(&self) -> crate::Result<()> { + self.window.close() + } + + /// Destroys this window. Similar to [`Self::close`] but does not emit any events and force close the window instead. + pub fn destroy(&self) -> crate::Result<()> { + self.window.destroy() + } + + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { + self.window.set_content_protected(protected) + } + + /// Resizes this window. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self.window.set_size(size.into()) + } + + /// Sets this window's minimum inner size. + pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { + self.window.set_min_size(size.map(|s| s.into())) + } + + /// Sets this window's maximum inner size. + pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { + self.window.set_max_size(size.map(|s| s.into())) + } + + /// Sets this window's minimum inner width. + pub fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { + self.window.set_size_constraints(constraints) + } + + /// Sets this window's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self.window.set_position(position) + } + + /// Bring the window to front and focus. + pub fn set_focus(&self) -> crate::Result<()> { + self.window.set_focus() + } + + /// Sets whether the window can be focused. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { + self.window.set_focusable(focusable) + } + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **iOS / Android:** Unsupported. + /// - **macOS**: Not implemented for the webview layer.. + /// - **Windows**: + /// - alpha channel is ignored for the window layer. + /// - On Windows 7, transparency is not supported and the alpha value will be ignored for the webview layer.. + /// - On Windows 8 and newer: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` for the webview layer. + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self.window.set_background_color(color)?; + self.webview.set_background_color(color) + } /// Sets the theme for this window. /// @@ -2248,7 +2318,7 @@ impl WebviewWindow { } } -/// Desktop webview setters and actions. +/// Desktop webview APIs. #[cfg(desktop)] impl WebviewWindow { /// Opens the dialog to prints the contents of the webview. @@ -2318,7 +2388,7 @@ impl WebviewWindow { /// ``` #[allow(clippy::needless_doctest_main)] // To avoid a large diff #[cfg(feature = "wry")] - #[cfg_attr(docsrs, doc(feature = "wry"))] + #[cfg_attr(docsrs, doc(cfg(feature = "wry")))] pub fn with_webview( &self, f: F, @@ -2375,6 +2445,18 @@ impl WebviewWindow { self.webview.eval(js) } + /// Evaluate JavaScript with callback function on this webview. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + pub fn eval_with_callback( + &self, + js: impl Into, + callback: impl Fn(String) + Send + 'static, + ) -> crate::Result<()> { + self.webview.eval_with_callback(js, callback) + } + /// Opens the developer tools window (Web Inspector). /// The devtools is only enabled on debug builds or with the `devtools` feature flag. /// @@ -2702,10 +2784,20 @@ impl ManagerBase for WebviewWindow { } fn runtime(&self) -> RuntimeOrDispatch<'_, R> { - self.webview.runtime() + self.window.runtime() } fn managed_app_handle(&self) -> &AppHandle { self.webview.managed_app_handle() } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.window.activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.window.scene_identifier()) + } } diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs index 4d0f938feb6d..a329391422d2 100644 --- a/crates/tauri/src/window/mod.rs +++ b/crates/tauri/src/window/mod.rs @@ -20,10 +20,7 @@ use crate::{ CursorIcon, image::Image, menu::{ContextMenu, Menu, MenuId}, - runtime::{ - UserAttentionType, - dpi::{Position, Size}, - }, + runtime::UserAttentionType, }; use crate::{ Emitter, EventLoopMessage, EventName, Listener, Manager, ResourceTable, Runtime, Theme, Webview, @@ -34,6 +31,7 @@ use crate::{ manager::{AppManager, EmitPayload}, runtime::{ RuntimeHandle, WindowDispatch, + dpi::{Position, Size}, monitor::Monitor as RuntimeMonitor, window::{DetachedWindow, PendingWindow, WindowBuilder as _}, }, @@ -129,6 +127,10 @@ unstable_struct!( #[cfg(desktop)] on_menu_event: Option>>, window_effects: Option, + #[cfg(target_os = "android")] + created_by_activity_name_set: bool, + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: bool, } ); @@ -215,6 +217,10 @@ async fn create_window(app: tauri::AppHandle) { #[cfg(desktop)] on_menu_event: None, window_effects: None, + #[cfg(target_os = "android")] + created_by_activity_name_set: false, + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: false, } } @@ -250,6 +256,10 @@ async fn reopen_window(app: tauri::AppHandle) { pub fn from_config(manager: &'a M, config: &WindowConfig) -> crate::Result { #[cfg_attr(not(windows), allow(unused_mut))] let mut builder = Self { + #[cfg(target_os = "android")] + created_by_activity_name_set: config.created_by_activity_name.is_some(), + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: config.requested_by_scene_identifier.is_some(), manager, label: config.label.clone(), window_effects: config.window_effects.clone(), @@ -345,12 +355,31 @@ tauri::Builder::::new() /// Creates a new window with an optional webview. fn build_internal( - self, + // mutable on Android + #[allow(unused_mut)] mut self, webview: Option>, ) -> crate::Result> { #[cfg(desktop)] let theme = self.window_builder.get_theme(); + #[cfg(target_os = "android")] + if !self.created_by_activity_name_set { + if let Some(manager_window_activity_name) = self.manager.activity_name() { + self.window_builder = self + .window_builder + .created_by_activity_name(manager_window_activity_name?); + } + } + + #[cfg(target_os = "ios")] + if !self.requested_by_scene_identifier_set { + if let Some(manager_window_scene_identifier) = self.manager.scene_identifier() { + self.window_builder = self + .window_builder + .requested_by_scene_identifier(manager_window_scene_identifier?); + } + } + let mut pending = PendingWindow::new(self.window_builder, self.label)?; if let Some(webview) = webview { pending.set_webview(webview); @@ -426,7 +455,7 @@ tauri::Builder::::new() } } -/// Desktop APIs. +/// Desktop APIs #[cfg(desktop)] #[cfg_attr(not(feature = "unstable"), allow(dead_code))] impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { @@ -444,44 +473,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// The initial position of the window in logical pixels. - #[must_use] - pub fn position(mut self, x: f64, y: f64) -> Self { - self.window_builder = self.window_builder.position(x, y); - self - } - - /// Window size in logical pixels. - #[must_use] - pub fn inner_size(mut self, width: f64, height: f64) -> Self { - self.window_builder = self.window_builder.inner_size(width, height); - self - } - - /// Window min inner size in logical pixels. - #[must_use] - pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { - self.window_builder = self.window_builder.min_inner_size(min_width, min_height); - self - } - - /// Window max inner size in logical pixels. - #[must_use] - pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { - self.window_builder = self.window_builder.max_inner_size(max_width, max_height); - self - } - - /// Window inner size constraints. - #[must_use] - pub fn inner_size_constraints( - mut self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> Self { - self.window_builder = self.window_builder.inner_size_constraints(constraints); - self - } - /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) /// on creation, which means the window size will be limited to `monitor size - taskbar size` /// @@ -512,14 +503,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Whether the window is resizable or not. - /// When resizable is set to false, native window's maximize button is automatically disabled. - #[must_use] - pub fn resizable(mut self, resizable: bool) -> Self { - self.window_builder = self.window_builder.resizable(resizable); - self - } - /// Whether the window's native maximize button is enabled or not. /// If resizable is set to false, this setting is ignored. /// @@ -557,13 +540,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// The title of the window in the title bar. - #[must_use] - pub fn title>(mut self, title: S) -> Self { - self.window_builder = self.window_builder.title(title); - self - } - /// Whether to start the window in fullscreen or not. #[must_use] pub fn fullscreen(mut self, fullscreen: bool) -> Self { @@ -571,31 +547,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Sets the window to be initially focused. - #[must_use] - #[deprecated( - since = "1.2.0", - note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." - )] - pub fn focus(mut self) -> Self { - self.window_builder = self.window_builder.focused(true); - self - } - - /// Whether the window will be initially focused or not. - #[must_use] - pub fn focused(mut self, focused: bool) -> Self { - self.window_builder = self.window_builder.focused(focused); - self - } - - /// Whether the window will be focusable or not. - #[must_use] - pub fn focusable(mut self, focusable: bool) -> Self { - self.window_builder = self.window_builder.focusable(focusable); - self - } - /// Whether the window should be maximized upon creation. #[must_use] pub fn maximized(mut self, maximized: bool) -> Self { @@ -603,37 +554,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Whether the window should be immediately visible upon creation. - #[must_use] - pub fn visible(mut self, visible: bool) -> Self { - self.window_builder = self.window_builder.visible(visible); - self - } - - /// Forces a theme or uses the system settings if None was provided. - /// - /// ## Platform-specific - /// - /// - **macOS**: Only supported on macOS 10.14+. - #[must_use] - pub fn theme(mut self, theme: Option) -> Self { - self.window_builder = self.window_builder.theme(theme); - self - } - - /// Whether the window should be transparent. If this is true, writing colors - /// with alpha values different than `1.0` will produce a transparent window. - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - #[cfg_attr( - docsrs, - doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api"))) - )] - #[must_use] - pub fn transparent(mut self, transparent: bool) -> Self { - self.window_builder = self.window_builder.transparent(transparent); - self - } - /// Whether the window should have borders and bars. #[must_use] pub fn decorations(mut self, decorations: bool) -> Self { @@ -668,13 +588,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Prevents the window contents from being captured by other apps. - #[must_use] - pub fn content_protected(mut self, protected: bool) -> Self { - self.window_builder = self.window_builder.content_protected(protected); - self - } - /// Sets the window icon. pub fn icon(mut self, icon: Image<'a>) -> crate::Result { self.window_builder = self.window_builder.icon(icon.into())?; @@ -901,106 +814,260 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { } } -impl> WindowBuilder<'_, R, M> { - /// Set the window and webview background color. - /// - /// ## Platform-specific: - /// - /// - **Windows**: alpha channel is ignored. +/// Window APIs. +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// The initial position of the window in logical pixels. #[must_use] - pub fn background_color(mut self, color: Color) -> Self { - self.window_builder = self.window_builder.background_color(color); + pub fn position(mut self, x: f64, y: f64) -> Self { + self.window_builder = self.window_builder.position(x, y); self } -} -/// A wrapper struct to hold the window menu state -/// and whether it is global per-app or specific to this window. -#[cfg(desktop)] -pub(crate) struct WindowMenu { - pub(crate) is_app_wide: bool, - pub(crate) menu: Menu, -} - -// TODO: expand these docs since this is a pretty important type -/// A window managed by Tauri. -/// -/// This type also implements [`Manager`] which allows you to manage other windows attached to -/// the same application. -#[default_runtime(crate::Wry, wry)] -pub struct Window { - /// The window created by the runtime. - pub(crate) window: DetachedWindow, - /// The manager to associate this window with. - pub(crate) manager: Arc>, - pub(crate) app_handle: AppHandle, - // The menu set for this window - #[cfg(desktop)] - pub(crate) menu: Arc>>>, - pub(crate) resources_table: Arc>, -} -impl std::fmt::Debug for Window { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Window") - .field("window", &self.window) - .field("manager", &self.manager) - .field("app_handle", &self.app_handle) - .finish() + /// Window size in logical pixels. + #[must_use] + pub fn inner_size(mut self, width: f64, height: f64) -> Self { + self.window_builder = self.window_builder.inner_size(width, height); + self } -} -impl raw_window_handle::HasWindowHandle for Window { - fn window_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - self.window.dispatcher.window_handle() + /// Window min inner size in logical pixels. + #[must_use] + pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.window_builder = self.window_builder.min_inner_size(min_width, min_height); + self } -} -impl raw_window_handle::HasDisplayHandle for Window { - fn display_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - self.app_handle.display_handle() + /// Window max inner size in logical pixels. + #[must_use] + pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.window_builder = self.window_builder.max_inner_size(max_width, max_height); + self } -} -impl Clone for Window { - fn clone(&self) -> Self { - Self { - window: self.window.clone(), - manager: self.manager.clone(), - app_handle: self.app_handle.clone(), - #[cfg(desktop)] - menu: self.menu.clone(), - resources_table: self.resources_table.clone(), - } + /// Window inner size constraints. + #[must_use] + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self.window_builder = self.window_builder.inner_size_constraints(constraints); + self } -} -impl Hash for Window { - /// Only use the [`Window`]'s label to represent its hash. - fn hash(&self, state: &mut H) { - self.window.label.hash(state) + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + pub fn resizable(mut self, resizable: bool) -> Self { + self.window_builder = self.window_builder.resizable(resizable); + self } -} -impl Eq for Window {} -impl PartialEq for Window { - /// Only use the [`Window`]'s label to compare equality. - fn eq(&self, other: &Self) -> bool { - self.window.label.eq(&other.window.label) + /// The title of the window in the title bar. + #[must_use] + pub fn title>(mut self, title: S) -> Self { + self.window_builder = self.window_builder.title(title); + self } -} -impl Manager for Window { - fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + /// Sets the window to be initially focused. + #[must_use] + #[deprecated( + since = "1.2.0", + note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." + )] + pub fn focus(mut self) -> Self { + self.window_builder = self.window_builder.focused(true); self - .resources_table - .lock() - .expect("poisoned window resources table") } -} + + /// Whether the window will be initially focused or not. + #[must_use] + pub fn focused(mut self, focused: bool) -> Self { + self.window_builder = self.window_builder.focused(focused); + self + } + + /// Whether the window will be focusable or not. + #[must_use] + pub fn focusable(mut self, focusable: bool) -> Self { + self.window_builder = self.window_builder.focusable(focusable); + self + } + + /// Whether the window should be immediately visible upon creation. + #[must_use] + pub fn visible(mut self, visible: bool) -> Self { + self.window_builder = self.window_builder.visible(visible); + self + } + + /// Forces a theme or uses the system settings if None was provided. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + #[must_use] + pub fn theme(mut self, theme: Option) -> Self { + self.window_builder = self.window_builder.theme(theme); + self + } + + /// Whether the window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + #[cfg_attr( + docsrs, + doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api"))) + )] + #[must_use] + pub fn transparent(mut self, transparent: bool) -> Self { + self.window_builder = self.window_builder.transparent(transparent); + self + } + + /// Prevents the window contents from being captured by other apps. + #[must_use] + pub fn content_protected(mut self, protected: bool) -> Self { + self.window_builder = self.window_builder.content_protected(protected); + self + } + + /// Set the window and webview background color. + /// + /// ## Platform-specific: + /// + /// - **Windows**: alpha channel is ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.window_builder = self.window_builder.background_color(color); + self + } +} + +#[cfg(target_os = "android")] +impl> WindowBuilder<'_, R, M> { + /// The name of the activity to create for this webview window. + pub fn activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.activity_name(class_name); + self + } + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + pub fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.created_by_activity_name_set = true; + self.window_builder = self.window_builder.created_by_activity_name(class_name); + self + } +} + +/// iOS specific APIs +#[cfg(target_os = "ios")] +impl> WindowBuilder<'_, R, M> { + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + pub fn requested_by_scene_identifier(mut self, identifier: String) -> Self { + self.requested_by_scene_identifier_set = true; + self.window_builder = self + .window_builder + .requested_by_scene_identifier(identifier); + self + } +} + +/// A wrapper struct to hold the window menu state +/// and whether it is global per-app or specific to this window. +#[cfg(desktop)] +pub(crate) struct WindowMenu { + pub(crate) is_app_wide: bool, + pub(crate) menu: Menu, +} + +// TODO: expand these docs since this is a pretty important type +/// A window managed by Tauri. +/// +/// This type also implements [`Manager`] which allows you to manage other windows attached to +/// the same application. +#[default_runtime(crate::Wry, wry)] +pub struct Window { + /// The window created by the runtime. + pub(crate) window: DetachedWindow, + /// The manager to associate this window with. + pub(crate) manager: Arc>, + pub(crate) app_handle: AppHandle, + // The menu set for this window + #[cfg(desktop)] + pub(crate) menu: Arc>>>, + pub(crate) resources_table: Arc>, +} + +impl std::fmt::Debug for Window { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Window") + .field("window", &self.window) + .field("manager", &self.manager) + .field("app_handle", &self.app_handle) + .finish() + } +} + +impl raw_window_handle::HasWindowHandle for Window { + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.window.dispatcher.window_handle() + } +} + +impl raw_window_handle::HasDisplayHandle for Window { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.app_handle.display_handle() + } +} + +impl Clone for Window { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + manager: self.manager.clone(), + app_handle: self.app_handle.clone(), + #[cfg(desktop)] + menu: self.menu.clone(), + resources_table: self.resources_table.clone(), + } + } +} + +impl Hash for Window { + /// Only use the [`Window`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.window.label.hash(state) + } +} + +impl Eq for Window {} +impl PartialEq for Window { + /// Only use the [`Window`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.window.label.eq(&other.window.label) + } +} + +impl Manager for Window { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window resources table") + } +} impl ManagerBase for Window { fn manager(&self) -> &AppManager { @@ -1018,6 +1085,16 @@ impl ManagerBase for Window { fn managed_app_handle(&self) -> &AppHandle { &self.app_handle } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.scene_identifier()) + } } impl<'de, R: Runtime> CommandArg<'de, R> for Window { @@ -1628,6 +1705,22 @@ impl Window { self.window.dispatcher.default_vbox().map_err(Into::into) } + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + pub fn activity_name(&self) -> crate::Result { + self.window.dispatcher.activity_name().map_err(Into::into) + } + + /// Returns the identifier of the UIScene tied to this window. + #[cfg(target_os = "ios")] + pub fn scene_identifier(&self) -> crate::Result { + self + .window + .dispatcher + .scene_identifier() + .map_err(Into::into) + } + /// Returns the current window theme. /// /// ## Platform-specific @@ -1654,36 +1747,8 @@ impl Window { } } -/// Desktop window setters and actions. -#[cfg(desktop)] +/// Window setters and actions. impl Window { - /// Centers the window. - pub fn center(&self) -> crate::Result<()> { - self.window.dispatcher.center().map_err(Into::into) - } - - /// Requests user attention to the window, this has no effect if the application - /// is already focused. How requesting for user attention manifests is platform dependent, - /// see `UserAttentionType` for details. - /// - /// Providing `None` will unset the request for user attention. Unsetting the request for - /// user attention might not be done automatically by the WM when the window receives input. - /// - /// ## Platform-specific - /// - /// - **macOS:** `None` has no effect. - /// - **Linux:** Urgency levels have the same effect. - pub fn request_user_attention( - &self, - request_type: Option, - ) -> crate::Result<()> { - self - .window - .dispatcher - .request_user_attention(request_type) - .map_err(Into::into) - } - /// Determines if this window should be resizable. /// When resizable is set to false, native window's maximize button is automatically disabled. pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { @@ -1694,49 +1759,6 @@ impl Window { .map_err(Into::into) } - /// Determines if this window's native maximize button should be enabled. - /// If resizable is set to false, this setting is ignored. - /// - /// ## Platform-specific - /// - /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. - /// - **Linux / iOS / Android:** Unsupported. - pub fn set_maximizable(&self, maximizable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_maximizable(maximizable) - .map_err(Into::into) - } - - /// Determines if this window's native minimize button should be enabled. - /// - /// ## Platform-specific - /// - /// - **Linux / iOS / Android:** Unsupported. - pub fn set_minimizable(&self, minimizable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_minimizable(minimizable) - .map_err(Into::into) - } - - /// Determines if this window's native close button should be enabled. - /// - /// ## Platform-specific - /// - /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. - /// Depending on the system, this function may not have any effect when called on a window that is already visible" - /// - **iOS / Android:** Unsupported. - pub fn set_closable(&self, closable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_closable(closable) - .map_err(Into::into) - } - /// Set this window's title. pub fn set_title(&self, title: &str) -> crate::Result<()> { self @@ -1755,26 +1777,6 @@ impl Window { .map_err(Into::into) } - /// Maximizes this window. - pub fn maximize(&self) -> crate::Result<()> { - self.window.dispatcher.maximize().map_err(Into::into) - } - - /// Un-maximizes this window. - pub fn unmaximize(&self) -> crate::Result<()> { - self.window.dispatcher.unmaximize().map_err(Into::into) - } - - /// Minimizes this window. - pub fn minimize(&self) -> crate::Result<()> { - self.window.dispatcher.minimize().map_err(Into::into) - } - - /// Un-minimizes this window. - pub fn unminimize(&self) -> crate::Result<()> { - self.window.dispatcher.unminimize().map_err(Into::into) - } - /// Show this window. pub fn show(&self) -> crate::Result<()> { self.window.dispatcher.show().map_err(Into::into) @@ -1785,7 +1787,7 @@ impl Window { self.window.dispatcher.hide().map_err(Into::into) } - /// Closes this window. It emits [`crate::RunEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. + /// Closes this window. It emits [`crate::WindowEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. pub fn close(&self) -> crate::Result<()> { self.window.dispatcher.close().map_err(Into::into) } @@ -1795,6 +1797,219 @@ impl Window { self.window.dispatcher.destroy().map_err(Into::into) } + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **Windows:** alpha channel is ignored. + /// - **iOS / Android:** Unsupported. + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_background_color(color) + .map_err(Into::into) + } + + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_content_protected(protected) + .map_err(Into::into) + } + + /// Resizes this window. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self + .window + .dispatcher + .set_size(size.into()) + .map_err(Into::into) + } + + /// Sets this window's minimum inner size. + pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_min_size(size.map(|s| s.into())) + .map_err(Into::into) + } + + /// Sets this window's maximum inner size. + pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_max_size(size.map(|s| s.into())) + .map_err(Into::into) + } + + /// Sets this window's minimum inner width. + pub fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_size_constraints(constraints) + .map_err(Into::into) + } + + /// Sets this window's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self + .window + .dispatcher + .set_position(position.into()) + .map_err(Into::into) + } + + /// Bring the window to front and focus. + pub fn set_focus(&self) -> crate::Result<()> { + self.window.dispatcher.set_focus().map_err(Into::into) + } + + /// Sets whether the window can be focused. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_focusable(focusable) + .map_err(Into::into) + } + + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + pub fn set_theme(&self, theme: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_theme(theme) + .map_err(Into::::into)?; + #[cfg(windows)] + if let (Some(menu), Ok(hwnd)) = (self.menu(), self.hwnd()) { + let raw_hwnd = hwnd.0 as isize; + self.run_on_main_thread(move || { + let _ = unsafe { + menu.inner().set_theme_for_hwnd( + raw_hwnd, + theme + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto), + ) + }; + })?; + }; + Ok(()) + } +} + +/// Desktop window setters and actions. +#[cfg(desktop)] +impl Window { + /// Centers the window. + pub fn center(&self) -> crate::Result<()> { + self.window.dispatcher.center().map_err(Into::into) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see `UserAttentionType` for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **macOS:** `None` has no effect. + /// - **Linux:** Urgency levels have the same effect. + pub fn request_user_attention( + &self, + request_type: Option, + ) -> crate::Result<()> { + self + .window + .dispatcher + .request_user_attention(request_type) + .map_err(Into::into) + } + + /// Determines if this window's native maximize button should be enabled. + /// If resizable is set to false, this setting is ignored. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_maximizable(&self, maximizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_maximizable(maximizable) + .map_err(Into::into) + } + + /// Determines if this window's native minimize button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_minimizable(&self, minimizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_minimizable(minimizable) + .map_err(Into::into) + } + + /// Determines if this window's native close button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + pub fn set_closable(&self, closable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_closable(closable) + .map_err(Into::into) + } + + /// Maximizes this window. + pub fn maximize(&self) -> crate::Result<()> { + self.window.dispatcher.maximize().map_err(Into::into) + } + + /// Un-maximizes this window. + pub fn unmaximize(&self) -> crate::Result<()> { + self.window.dispatcher.unmaximize().map_err(Into::into) + } + + /// Minimizes this window. + pub fn minimize(&self) -> crate::Result<()> { + self.window.dispatcher.minimize().map_err(Into::into) + } + + /// Un-minimizes this window. + pub fn unminimize(&self) -> crate::Result<()> { + self.window.dispatcher.unminimize().map_err(Into::into) + } + /// Determines if this window should be [decorated]. /// /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration @@ -1897,77 +2112,6 @@ tauri::Builder::::new() .map_err(Into::into) } - /// Sets the window background color. - /// - /// ## Platform-specific: - /// - /// - **Windows:** alpha channel is ignored. - /// - **iOS / Android:** Unsupported. - pub fn set_background_color(&self, color: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_background_color(color) - .map_err(Into::into) - } - - /// Prevents the window contents from being captured by other apps. - pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_content_protected(protected) - .map_err(Into::into) - } - - /// Resizes this window. - pub fn set_size>(&self, size: S) -> crate::Result<()> { - self - .window - .dispatcher - .set_size(size.into()) - .map_err(Into::into) - } - - /// Sets this window's minimum inner size. - pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_min_size(size.map(|s| s.into())) - .map_err(Into::into) - } - - /// Sets this window's maximum inner size. - pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_max_size(size.map(|s| s.into())) - .map_err(Into::into) - } - - /// Sets this window's minimum inner width. - pub fn set_size_constraints( - &self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> crate::Result<()> { - self - .window - .dispatcher - .set_size_constraints(constraints) - .map_err(Into::into) - } - - /// Sets this window's position. - pub fn set_position>(&self, position: Pos) -> crate::Result<()> { - self - .window - .dispatcher - .set_position(position.into()) - .map_err(Into::into) - } - /// Determines if this window should be fullscreen. pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { self @@ -2000,25 +2144,6 @@ tauri::Builder::::new() self.set_fullscreen(enable) } - /// Bring the window to front and focus. - pub fn set_focus(&self) -> crate::Result<()> { - self.window.dispatcher.set_focus().map_err(Into::into) - } - - /// Sets whether the window can be focused. - /// - /// ## Platform-specific - /// - /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. - /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. - pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_focusable(focusable) - .map_err(Into::into) - } - /// Sets this window' icon. pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { self @@ -2197,35 +2322,6 @@ tauri::Builder::::new() .set_traffic_light_position(position) .map_err(Into::into) } - - /// Sets the theme for this window. - /// - /// ## Platform-specific - /// - /// - **Linux / macOS**: Theme is app-wide and not specific to this window. - /// - **iOS / Android:** Unsupported. - pub fn set_theme(&self, theme: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_theme(theme) - .map_err(Into::::into)?; - #[cfg(windows)] - if let (Some(menu), Ok(hwnd)) = (self.menu(), self.hwnd()) { - let raw_hwnd = hwnd.0 as isize; - self.run_on_main_thread(move || { - let _ = unsafe { - menu.inner().set_theme_for_hwnd( - raw_hwnd, - theme - .map(crate::menu::map_to_menu_theme) - .unwrap_or(muda::MenuTheme::Auto), - ) - }; - })?; - }; - Ok(()) - } } /// Progress bar state. diff --git a/crates/tauri/src/window/plugin.rs b/crates/tauri/src/window/plugin.rs index 698d5f160f4b..0bea76cb59ce 100644 --- a/crates/tauri/src/window/plugin.rs +++ b/crates/tauri/src/window/plugin.rs @@ -5,23 +5,56 @@ //! The tauri plugin to create and manipulate windows from JS. use crate::{ - Runtime, + Runtime, Window, plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, }; -#[cfg(desktop)] -mod desktop_commands { - use tauri_runtime::{ResizeDirection, window::WindowSizeConstraints}; - use tauri_utils::TitleBarStyle; +fn get_window(window: Window, label: Option) -> crate::Result> { + match label { + Some(l) if !l.is_empty() => window + .manager() + .get_window(&l) + .ok_or(crate::Error::WindowNotFound), + _ => Ok(window), + } +} +macro_rules! getter { + ($cmd: ident, $ret: ty) => { + #[command(root = "crate")] + pub async fn $cmd(window: Window, label: Option) -> crate::Result<$ret> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; +} + +macro_rules! setter { + ($cmd: ident) => { + #[command(root = "crate")] + pub async fn $cmd(window: Window, label: Option) -> crate::Result<()> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; + + ($cmd: ident, $input: ty) => { + #[command(root = "crate")] + pub async fn $cmd( + window: Window, + label: Option, + value: $input, + ) -> crate::Result<()> { + get_window(window, label)?.$cmd(value).map_err(Into::into) + } + }; +} + +mod commands { + use tauri_runtime::window::WindowSizeConstraints; use super::*; use crate::{ - AppHandle, CursorIcon, Manager, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, - UserAttentionType, Webview, Window, command, - sealed::ManagerBase, - utils::config::{WindowConfig, WindowEffectsConfig}, - window::Color, - window::{ProgressBarState, WindowBuilder}, + AppHandle, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, Window, command, + sealed::ManagerBase, utils::config::WindowConfig, window::Color, window::WindowBuilder, }; #[command(root = "crate")] @@ -30,110 +63,101 @@ mod desktop_commands { } #[command(root = "crate")] - pub async fn create(app: AppHandle, options: WindowConfig) -> crate::Result<()> { - WindowBuilder::from_config(&app, &options)?.build()?; + pub async fn create(window: Window, options: WindowConfig) -> crate::Result<()> { + WindowBuilder::from_config(&window, &options)?.build()?; Ok(()) } - fn get_window(window: Window, label: Option) -> crate::Result> { - match label { - Some(l) if !l.is_empty() => window - .manager() - .get_window(&l) - .ok_or(crate::Error::WindowNotFound), - _ => Ok(window), - } - } - - macro_rules! getter { - ($cmd: ident, $ret: ty) => { - #[command(root = "crate")] - pub async fn $cmd( - window: Window, - label: Option, - ) -> crate::Result<$ret> { - get_window(window, label)?.$cmd().map_err(Into::into) - } - }; - } - - macro_rules! setter { - ($cmd: ident) => { - #[command(root = "crate")] - pub async fn $cmd(window: Window, label: Option) -> crate::Result<()> { - get_window(window, label)?.$cmd().map_err(Into::into) - } - }; - - ($cmd: ident, $input: ty) => { - #[command(root = "crate")] - pub async fn $cmd( - window: Window, - label: Option, - value: $input, - ) -> crate::Result<()> { - get_window(window, label)?.$cmd(value).map_err(Into::into) - } - }; - } - getter!(scale_factor, f64); getter!(inner_position, PhysicalPosition); getter!(outer_position, PhysicalPosition); getter!(inner_size, PhysicalSize); getter!(outer_size, PhysicalSize); - getter!(is_fullscreen, bool); - getter!(is_minimized, bool); - getter!(is_maximized, bool); getter!(is_focused, bool); - getter!(is_decorated, bool); getter!(is_resizable, bool); - getter!(is_maximizable, bool); - getter!(is_minimizable, bool); - getter!(is_closable, bool); getter!(is_visible, bool); getter!(is_enabled, bool); getter!(title, String); + getter!(theme, Theme); + #[cfg(target_os = "android")] + getter!(activity_name, String); + #[cfg(target_os = "ios")] + getter!(scene_identifier, String); + + setter!(set_resizable, bool); + setter!(set_title, &str); + setter!(show); + setter!(hide); + setter!(close); + setter!(destroy); + setter!(set_content_protected, bool); + setter!(set_size, Size); + setter!(set_min_size, Option); + setter!(set_max_size, Option); + setter!(set_position, Position); + setter!(set_focus); + setter!(set_focusable, bool); + setter!(set_background_color, Option); + setter!(set_size_constraints, WindowSizeConstraints); + setter!(set_theme, Option); + setter!(set_enabled, bool); + getter!(current_monitor, Option); getter!(primary_monitor, Option); getter!(available_monitors, Vec); + + #[command(root = "crate")] + pub async fn monitor_from_point( + window: Window, + label: Option, + x: f64, + y: f64, + ) -> crate::Result> { + let window = get_window(window, label)?; + window.monitor_from_point(x, y) + } +} + +#[cfg(desktop)] +mod desktop_commands { + use tauri_runtime::ResizeDirection; + use tauri_utils::TitleBarStyle; + + use super::*; + use crate::{ + CursorIcon, Manager, PhysicalPosition, Position, UserAttentionType, Webview, command, + utils::config::WindowEffectsConfig, window::ProgressBarState, + }; + + getter!(is_fullscreen, bool); + getter!(is_minimized, bool); + getter!(is_maximized, bool); + getter!(is_decorated, bool); + getter!(is_maximizable, bool); + getter!(is_minimizable, bool); + getter!(is_closable, bool); getter!(cursor_position, PhysicalPosition); - getter!(theme, Theme); getter!(is_always_on_top, bool); setter!(center); setter!(request_user_attention, Option); - setter!(set_resizable, bool); setter!(set_maximizable, bool); setter!(set_minimizable, bool); setter!(set_closable, bool); - setter!(set_title, &str); setter!(maximize); setter!(unmaximize); setter!(minimize); setter!(unminimize); - setter!(show); - setter!(hide); - setter!(close); - setter!(destroy); setter!(set_decorations, bool); setter!(set_shadow, bool); setter!(set_effects, Option); setter!(set_always_on_top, bool); setter!(set_always_on_bottom, bool); - setter!(set_content_protected, bool); - setter!(set_size, Size); - setter!(set_min_size, Option); - setter!(set_max_size, Option); - setter!(set_position, Position); setter!(set_fullscreen, bool); setter!(set_simple_fullscreen, bool); - setter!(set_focus); - setter!(set_focusable, bool); setter!(set_skip_taskbar, bool); setter!(set_cursor_grab, bool); setter!(set_cursor_visible, bool); - setter!(set_background_color, Option); setter!(set_cursor_icon, CursorIcon); setter!(set_cursor_position, Position); setter!(set_ignore_cursor_events, bool); @@ -145,9 +169,6 @@ mod desktop_commands { setter!(set_badge_label, Option); setter!(set_visible_on_all_workspaces, bool); setter!(set_title_bar_style, TitleBarStyle); - setter!(set_size_constraints, WindowSizeConstraints); - setter!(set_theme, Option); - setter!(set_enabled, bool); #[command(root = "crate")] #[cfg(target_os = "windows")] @@ -207,17 +228,6 @@ mod desktop_commands { } Ok(()) } - - #[command(root = "crate")] - pub async fn monitor_from_point( - window: Window, - label: Option, - x: f64, - y: f64, - ) -> crate::Result> { - let window = get_window(window, label)?; - window.monitor_from_point(x, y) - } } /// Initializes the plugin. @@ -243,96 +253,95 @@ pub fn init() -> TauriPlugin { Builder::new("window") .js_init_script(init_script) - .invoke_handler( - #[cfg(desktop)] - crate::generate_handler![ - #![plugin(window)] - desktop_commands::create, - // getters - desktop_commands::get_all_windows, - desktop_commands::scale_factor, - desktop_commands::inner_position, - desktop_commands::outer_position, - desktop_commands::inner_size, - desktop_commands::outer_size, - desktop_commands::is_fullscreen, - desktop_commands::is_minimized, - desktop_commands::is_maximized, - desktop_commands::is_focused, - desktop_commands::is_decorated, - desktop_commands::is_resizable, - desktop_commands::is_maximizable, - desktop_commands::is_minimizable, - desktop_commands::is_closable, - desktop_commands::is_visible, - desktop_commands::is_enabled, - desktop_commands::title, - desktop_commands::current_monitor, - desktop_commands::primary_monitor, - desktop_commands::monitor_from_point, - desktop_commands::available_monitors, - desktop_commands::cursor_position, - desktop_commands::theme, - desktop_commands::is_always_on_top, - // setters - desktop_commands::center, - desktop_commands::request_user_attention, - desktop_commands::set_resizable, - desktop_commands::set_maximizable, - desktop_commands::set_minimizable, - desktop_commands::set_closable, - desktop_commands::set_title, - desktop_commands::maximize, - desktop_commands::unmaximize, - desktop_commands::minimize, - desktop_commands::unminimize, - desktop_commands::show, - desktop_commands::hide, - desktop_commands::close, - desktop_commands::destroy, - desktop_commands::set_decorations, - desktop_commands::set_shadow, - desktop_commands::set_effects, - desktop_commands::set_always_on_top, - desktop_commands::set_always_on_bottom, - desktop_commands::set_content_protected, - desktop_commands::set_size, - desktop_commands::set_min_size, - desktop_commands::set_max_size, - desktop_commands::set_size_constraints, - desktop_commands::set_position, - desktop_commands::set_fullscreen, - desktop_commands::set_simple_fullscreen, - desktop_commands::set_focus, - desktop_commands::set_focusable, - desktop_commands::set_enabled, - desktop_commands::set_skip_taskbar, - desktop_commands::set_cursor_grab, - desktop_commands::set_cursor_visible, - desktop_commands::set_cursor_icon, - desktop_commands::set_cursor_position, - desktop_commands::set_ignore_cursor_events, - desktop_commands::start_dragging, - desktop_commands::start_resize_dragging, - desktop_commands::set_badge_count, - #[cfg(target_os = "macos")] - desktop_commands::set_badge_label, - desktop_commands::set_progress_bar, - #[cfg(target_os = "windows")] - desktop_commands::set_overlay_icon, - desktop_commands::set_icon, - desktop_commands::set_visible_on_all_workspaces, - desktop_commands::set_background_color, - desktop_commands::set_title_bar_style, - desktop_commands::set_theme, - desktop_commands::toggle_maximize, - desktop_commands::internal_toggle_maximize, - ], - #[cfg(mobile)] - |invoke| { - invoke.resolver.reject("Window API not available on mobile"); - true - }, - ) + .invoke_handler(crate::generate_handler![ + #![plugin(window)] + commands::create, + // getters + commands::get_all_windows, + commands::scale_factor, + commands::inner_position, + commands::outer_position, + commands::inner_size, + commands::outer_size, + commands::is_focused, + commands::is_resizable, + commands::is_visible, + commands::is_enabled, + commands::title, + commands::theme, + #[cfg(target_os = "android")] + commands::activity_name, + #[cfg(target_os = "ios")] + commands::scene_identifier, + + commands::set_resizable, + commands::set_title, + commands::show, + commands::hide, + commands::close, + commands::destroy, + commands::set_content_protected, + commands::set_size, + commands::set_min_size, + commands::set_max_size, + commands::set_position, + commands::set_size_constraints, + commands::set_focus, + commands::set_focusable, + commands::set_enabled, + commands::set_background_color, + commands::set_theme, + commands::current_monitor, + commands::primary_monitor, + commands::monitor_from_point, + commands::available_monitors, + + #[cfg(desktop)] desktop_commands::is_fullscreen, + #[cfg(desktop)] desktop_commands::is_minimized, + #[cfg(desktop)] desktop_commands::is_maximized, + #[cfg(desktop)] desktop_commands::is_decorated, + #[cfg(desktop)] desktop_commands::is_maximizable, + #[cfg(desktop)] desktop_commands::is_minimizable, + #[cfg(desktop)] desktop_commands::is_closable, + + #[cfg(desktop)] desktop_commands::cursor_position, + #[cfg(desktop)] desktop_commands::is_always_on_top, + // setters + #[cfg(desktop)] desktop_commands::center, + #[cfg(desktop)] desktop_commands::request_user_attention, + #[cfg(desktop)] desktop_commands::set_maximizable, + #[cfg(desktop)] desktop_commands::set_minimizable, + #[cfg(desktop)] desktop_commands::set_closable, + #[cfg(desktop)] desktop_commands::maximize, + #[cfg(desktop)] desktop_commands::unmaximize, + #[cfg(desktop)] desktop_commands::minimize, + #[cfg(desktop)] desktop_commands::unminimize, + #[cfg(desktop)] desktop_commands::set_decorations, + #[cfg(desktop)] desktop_commands::set_shadow, + #[cfg(desktop)] desktop_commands::set_effects, + #[cfg(desktop)] desktop_commands::set_always_on_top, + #[cfg(desktop)] desktop_commands::set_always_on_bottom, + #[cfg(desktop)] desktop_commands::set_fullscreen, + #[cfg(desktop)] desktop_commands::set_simple_fullscreen, + #[cfg(desktop)] desktop_commands::set_skip_taskbar, + #[cfg(desktop)] desktop_commands::set_cursor_grab, + #[cfg(desktop)] desktop_commands::set_cursor_visible, + #[cfg(desktop)] desktop_commands::set_cursor_icon, + #[cfg(desktop)] desktop_commands::set_cursor_position, + #[cfg(desktop)] desktop_commands::set_ignore_cursor_events, + #[cfg(desktop)] desktop_commands::start_dragging, + #[cfg(desktop)] desktop_commands::start_resize_dragging, + #[cfg(desktop)] desktop_commands::set_badge_count, + #[cfg(target_os = "macos")] + #[cfg(desktop)] desktop_commands::set_badge_label, + #[cfg(desktop)] desktop_commands::set_progress_bar, + #[cfg(target_os = "windows")] + #[cfg(desktop)] desktop_commands::set_overlay_icon, + #[cfg(desktop)] desktop_commands::set_icon, + #[cfg(desktop)] desktop_commands::set_visible_on_all_workspaces, + #[cfg(desktop)] desktop_commands::set_title_bar_style, + #[cfg(desktop)] desktop_commands::toggle_maximize, + #[cfg(desktop)] desktop_commands::internal_toggle_maximize, + ]) .build() } diff --git a/crates/tests/acl/Cargo.toml b/crates/tests/acl/Cargo.toml index 0fe970b9990d..9e4f45b903b7 100644 --- a/crates/tests/acl/Cargo.toml +++ b/crates/tests/acl/Cargo.toml @@ -11,6 +11,6 @@ rust-version.workspace = true publish = false [dev-dependencies] -tauri-utils = { path = "../../tauri-utils/", features = ["build"] } +tauri-utils = { path = "../../tauri-utils/", features = ["build-2"] } serde_json = "1" insta = "1" diff --git a/examples/api/package.json b/examples/api/package.json index 84db2ff41f82..d51a35a93bc3 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -13,12 +13,12 @@ "@tauri-apps/api": "../../packages/api/dist" }, "devDependencies": { - "@iconify-json/codicon": "^1.2.47", + "@iconify-json/codicon": "^1.2.49", "@iconify-json/ph": "^1.2.2", - "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@unocss/extractor-svelte": "^66.6.5", - "svelte": "^5.53.7", - "unocss": "^66.6.5", - "vite": "^7.3.1" + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@unocss/extractor-svelte": "^66.6.6", + "svelte": "^5.53.11", + "unocss": "^66.6.6", + "vite": "^8.0.5" } } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 74020a4179c3..c0c26af6b52c 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -18,9 +18,11 @@ use tauri::{ use tauri::{Manager, RunEvent}; use tauri_plugin_sample::{PingRequest, SampleExt}; -#[cfg(feature = "cef")] +#[cfg(test)] +type TauriRuntime = tauri::test::MockRuntime; +#[cfg(all(not(test), feature = "cef"))] type TauriRuntime = tauri::Cef; -#[cfg(not(feature = "cef"))] +#[cfg(all(not(test), not(feature = "cef")))] type TauriRuntime = tauri::Wry; #[derive(Clone, Serialize)] @@ -37,7 +39,7 @@ pub struct PopupMenu(#[allow(dead_code)] tauri::menu::Menu); #[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(feature = "cef", tauri::cef_entry_point)] pub fn run() { - run_app(tauri::Builder::::default(), |_app| {}); + run_app(tauri::Builder::::new(), |_app| {}); } pub fn run_app) + Send + 'static>( @@ -95,7 +97,7 @@ pub fn run_app) + Send + 'static>( let number = created_window_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let builder = tauri::WebviewWindowBuilder::new( + let builder = WebviewWindowBuilder::new( &app_, format!("new-{number}"), tauri::WebviewUrl::External(if cfg!(feature = "cef") { @@ -229,18 +231,19 @@ pub fn run_app) + Send + 'static>( #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Regular); + #[cfg(target_os = "ios")] + let mut counter = 0; app.run(move |_app_handle, _event| { - #[cfg(all(desktop, not(test)))] + #[cfg(not(test))] match &_event { #[cfg(not(feature = "cef"))] - RunEvent::ExitRequested { api, code, .. } => { + RunEvent::ExitRequested { api, code, .. } if code.is_none() => { // Keep the event loop running even if all windows are closed // This allow us to catch tray icon events when there is no window // if we manually requested an exit (code is Some(_)) we will let it go through - if code.is_none() { - api.prevent_exit(); - } + api.prevent_exit(); } + #[cfg(desktop)] RunEvent::WindowEvent { event: tauri::WindowEvent::CloseRequested { api, .. }, label, @@ -256,6 +259,20 @@ pub fn run_app) + Send + 'static>( .destroy() .unwrap(); } + #[cfg(target_os = "ios")] + RunEvent::SceneRequested { .. } => { + counter += 1; + WebviewWindowBuilder::new( + _app_handle, + format!("main-from-scene-{counter}"), + WebviewUrl::default(), + ) + .build() + .unwrap(); + } + RunEvent::Opened { urls } => { + println!("opened urls: {:?}", urls); + } _ => (), } }) diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index b40404c820ad..b99e62599e81 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -75,6 +75,37 @@ }, "bundle": { "active": true, + "fileAssociations": [ + { + "ext": ["png"], + "mimeType": "image/png", + "rank": "Default" + }, + { + "ext": ["jpg", "jpeg"], + "mimeType": "image/jpeg", + "rank": "Alternate" + }, + { + "ext": ["gif"], + "mimeType": "image/gif", + "rank": "Owner" + }, + { + "ext": ["taurijson"], + "exportedType": { + "identifier": "com.tauri.dev-file-associations-demo.taurijson", + "conformsTo": ["public.json"] + } + }, + { + "ext": ["taurid"], + "exportedType": { + "identifier": "com.tauri.dev-file-associations-demo.tauridata", + "conformsTo": ["public.data"] + } + } + ], "icon": [ "../../.icons/32x32.png", "../../.icons/128x128.png", diff --git a/examples/api/src/components/MenuItemBuilder.svelte b/examples/api/src/components/MenuItemBuilder.svelte index d907e0951a7b..0f93436978a4 100644 --- a/examples/api/src/components/MenuItemBuilder.svelte +++ b/examples/api/src/components/MenuItemBuilder.svelte @@ -32,7 +32,8 @@ 'ShowAll', 'CloseWindow', 'Quit', - 'Services' + 'Services', + 'BringAllToFront' ] function onKindChange(event) { diff --git a/examples/commands/commands.rs b/examples/commands/commands.rs index 022b1241d012..4f2a814aa0e1 100644 --- a/examples/commands/commands.rs +++ b/examples/commands/commands.rs @@ -25,3 +25,8 @@ pub fn simple_command(the_argument: String) { pub fn stateful_command(the_argument: Option, state: State<'_, super::MyState>) { println!("{:?} {:?}", the_argument, state.inner()); } + +#[command(rename = "renamed_command_in_mod_new")] +pub fn renamed_command_in_mod() { + println!("renamed command in mod called"); +} diff --git a/examples/commands/index.html b/examples/commands/index.html index aed95c21ca7d..ef95405fd89a 100644 --- a/examples/commands/index.html +++ b/examples/commands/index.html @@ -63,7 +63,11 @@

Tauri Commands

{ name: 'command_arguments_tuple_struct', args: { inlinePerson: ['ferris', 6] } - } + }, + { name: 'renamed_command' }, + { name: 'renamed_command_new' }, + { name: 'renamed_command_in_mod' }, + { name: 'renamed_command_in_mod_new' } ] for (const command of commands) { diff --git a/examples/commands/main.rs b/examples/commands/main.rs index 474b07216f78..a6b49b949ded 100644 --- a/examples/commands/main.rs +++ b/examples/commands/main.rs @@ -6,7 +6,7 @@ // we move some basic commands to a separate module just to show it works mod commands; -use commands::{cmd, invoke, message, resolver}; +use commands::{cmd, invoke, message, renamed_command_in_mod, resolver}; use serde::Deserialize; use tauri::{ @@ -187,6 +187,11 @@ fn command_arguments_wild(_: Window) { println!("we saw the wildcard!") } +#[command(rename = "renamed_command_new")] +fn renamed_command() { + println!("renamed command called") +} + #[derive(Deserialize)] struct Person<'a> { name: &'a str, @@ -251,6 +256,8 @@ fn main() { future_simple_command, async_stateful_command, command_arguments_wild, + renamed_command, + renamed_command_in_mod, command_arguments_struct, simple_command_with_result, async_simple_command_snake, diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 5af686715ac4..078cb72cd8d3 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## \[2.11.0] + +### New Features + +- [`074299c08`](https://www.github.com/tauri-apps/tauri/commit/074299c08dd99d2e1c57796f55ab24bc1d3976cc) ([#14307](https://www.github.com/tauri-apps/tauri/pull/14307)) Add Bring All to Front predefined menu item type +- [`a12142a48`](https://www.github.com/tauri-apps/tauri/commit/a12142a481f7a19b69e88ee36a438b1db71b36f5) ([#14357](https://www.github.com/tauri-apps/tauri/pull/14357)) Add macos support for setting the icon and icon template state in the same step of the main thread, to prevent flickering. +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. + ## \[2.10.1] ### Bug Fixes diff --git a/packages/api/package.json b/packages/api/package.json index db3744fc2446..367f80a6ad4d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/api", - "version": "2.10.1", + "version": "2.11.0", "description": "Tauri API definitions", "funding": { "type": "opencollective", @@ -55,9 +55,9 @@ "eslint-plugin-security": "4.0.0", "fast-glob": "3.3.3", "globals": "^17.4.0", - "rollup": "4.59.0", + "rollup": "4.60.3", "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.56.1" + "typescript": "^6.0.0", + "typescript-eslint": "^8.58.2" } } diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 55b28043631f..82cc51859224 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -274,6 +274,10 @@ async function onBackButtonPress( ) } +async function supportsMultipleWindows(): Promise { + return invoke('plugin:app|supports_multiple_windows') +} + export { getName, getVersion, @@ -288,5 +292,6 @@ export { setDockVisibility, getBundleType, type OnBackButtonPressPayload, - onBackButtonPress + onBackButtonPress, + supportsMultipleWindows } diff --git a/packages/api/src/event.ts b/packages/api/src/event.ts index 1cf0f542a32d..7d8da18c83fe 100644 --- a/packages/api/src/event.ts +++ b/packages/api/src/event.ts @@ -65,6 +65,8 @@ enum TauriEvent { WINDOW_SCALE_FACTOR_CHANGED = 'tauri://scale-change', WINDOW_THEME_CHANGED = 'tauri://theme-changed', WINDOW_CREATED = 'tauri://window-created', + WINDOW_SUSPENDED = 'tauri://suspended', + WINDOW_RESUMED = 'tauri://resumed', WEBVIEW_CREATED = 'tauri://webview-created', DRAG_ENTER = 'tauri://drag-enter', DRAG_OVER = 'tauri://drag-over', diff --git a/packages/api/src/menu/predefinedMenuItem.ts b/packages/api/src/menu/predefinedMenuItem.ts index fed9a543facc..1517a35fa970 100644 --- a/packages/api/src/menu/predefinedMenuItem.ts +++ b/packages/api/src/menu/predefinedMenuItem.ts @@ -102,6 +102,7 @@ export interface PredefinedMenuItemOptions { | 'CloseWindow' | 'Quit' | 'Services' + | 'BringAllToFront' | { About: AboutMetadata | null } diff --git a/packages/api/src/tray.ts b/packages/api/src/tray.ts index fabe3e3567bf..c7bce20abb65 100644 --- a/packages/api/src/tray.ts +++ b/packages/api/src/tray.ts @@ -296,6 +296,31 @@ export class TrayIcon extends Resource { }) } + /** + * Sets a new tray icon and template status atomically. **macOS only**. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + async setIconWithAsTemplate( + icon: string | Image | Uint8Array | ArrayBuffer | number[] | null, + asTemplate: boolean + ): Promise { + let trayIcon = null + if (icon) { + trayIcon = transformImage(icon) + } + return invoke('plugin:tray|set_icon_with_as_template', { + rid: this.rid, + icon: trayIcon, + asTemplate + }) + } + /** * Disable or enable showing the tray menu on left click. * diff --git a/packages/api/src/webview.ts b/packages/api/src/webview.ts index 47a497fb6c84..968983c02eda 100644 --- a/packages/api/src/webview.ts +++ b/packages/api/src/webview.ts @@ -897,6 +897,25 @@ interface WebviewOptions { * - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. */ scrollBarStyle?: ScrollBarStyle + /** + * Controls the WebView's browser-level general autofill behavior. + * + * **This option does not disable password or credit card autofill.** + * + * When set to `false`, the WebView will not automatically populate general form + * fields using previously stored data such as addresses or contact information. + * + * If not specified, this is `true` by default. + * + * ## Platform-specific + * + * - **Windows**: Supported. WebView2's autofill feature (called "Suggestions") + * may not honor `autocomplete="off"` on input elements in some cases. + * - **Linux / Android / iOS / macOS**: Unsupported and performs no operation. + * + * @since 2.11.0 + */ + generalAutofillEnabled?: boolean } export { Webview, getCurrentWebview, getAllWebviews } diff --git a/packages/api/src/window.ts b/packages/api/src/window.ts index 8a11d2cc3cb6..a6b157ad84d3 100644 --- a/packages/api/src/window.ts +++ b/packages/api/src/window.ts @@ -820,6 +820,18 @@ class Window { }) } + async activityName(): Promise { + return invoke('plugin:window|activity_name', { + label: this.label + }) + } + + async sceneIdentifier(): Promise { + return invoke('plugin:window|scene_identifier', { + label: this.label + }) + } + // Setters /** @@ -2511,6 +2523,23 @@ interface WindowOptions { * - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. */ scrollBarStyle?: ScrollBarStyle + /** + * The name of the Android activity to create for this window. + */ + activityName?: string + /** + * The name of the Android activity that is creating this webview window. + * + * This is important to determine which stack the activity will belong to. + */ + createdByActivityName?: string + /** + * Sets the identifier of the UIScene that is requesting the creation of this new scene, + * establishing a relationship between the two scenes. + * + * By default the system uses the foreground scene. + */ + requestedBySceneIdentifier?: string } function mapMonitor(m: Monitor | null): Monitor | null { diff --git a/packages/cli/.cef-cli-version b/packages/cli/.cef-cli-version new file mode 100644 index 000000000000..0c05817830bf --- /dev/null +++ b/packages/cli/.cef-cli-version @@ -0,0 +1 @@ +3.0.0-alpha.3 diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 236ecc1d3e69..23d2df879996 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-cli@2.11.1` + +## \[2.11.0] + +### New Features + +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`764b9139a`](https://www.github.com/tauri-apps/tauri/commit/764b9139a32de149d8a914a6b5ec6cd1937c64eb) ([#14313](https://www.github.com/tauri-apps/tauri/pull/14313)) Prompt to restart the Android emulator if it is not connected to adb. +- [`5dc2cee60`](https://www.github.com/tauri-apps/tauri/commit/5dc2cee60370665af88c185684432e425b1c987d) ([#14793](https://www.github.com/tauri-apps/tauri/pull/14793)) Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version` + + Notes: + + - For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION` + - For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change + +### Enhancements + +- [`be0e4bd2d`](https://www.github.com/tauri-apps/tauri/commit/be0e4bd2da02eb6cc75a8dc7c81663277e64c590) ([#15218](https://www.github.com/tauri-apps/tauri/pull/15218)) Added Vietnamese translations for the NSIS installer +- [`8718d0816`](https://www.github.com/tauri-apps/tauri/commit/8718d08163f074dfc53387ebd1d823f9c28280ee) ([#15033](https://www.github.com/tauri-apps/tauri/pull/15033)) Show the context before prompting for updater signing key password + +### Bug Fixes + +- [`fcb702ec4`](https://www.github.com/tauri-apps/tauri/commit/fcb702ec4d924e81943efaeebea8d3edb7289c33) ([#14954](https://www.github.com/tauri-apps/tauri/pull/14954)) Fix `build --bundles` to allow `nsis` arg in linux+macOS +- [`80c1425af`](https://www.github.com/tauri-apps/tauri/commit/80c1425af86058b1fc9489a30f778b6288d79b6b) ([#14921](https://www.github.com/tauri-apps/tauri/pull/14921)) Fix iOS build failure when `Metal Toolchain` is installed by using explicit `$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain` path instead of `$(TOOLCHAIN_DIR)` for Swift library search paths. + +### What's Changed + +- [`9979cde1c`](https://www.github.com/tauri-apps/tauri/commit/9979cde1c5534dafb1a07cc4dc2bc280d15d2f66) ([#15175](https://www.github.com/tauri-apps/tauri/pull/15175)) Update NSIS installer Italian translations + +### Dependencies + +- Upgraded to `tauri-cli@2.11.0` + ## \[2.10.1] ### Bug Fixes diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index f4c989f3ec8d..3aab1abc6253 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -7,13 +7,15 @@ version = "0.0.0" crate-type = ["cdylib"] [dependencies] -napi = "3" -napi-derive = "3" +napi = "=3.4" +napi-sys = "=3.0" +napi-derive-backend = "=3.0.0" +napi-derive = "=3.3.0" tauri-cli = { path = "../../crates/tauri-cli", default-features = false } log = "0.4.21" [build-dependencies] -napi-build = "2.2" +napi-build = "=2.2" [features] default = ["tauri-cli/default"] diff --git a/packages/cli/index.js b/packages/cli/index.js index c9e399696117..9034ed8cd24e 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -81,8 +81,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-android-arm64') const bindingPackageVersion = require('@tauri-apps/cli-android-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -97,8 +97,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-android-arm-eabi') const bindingPackageVersion = require('@tauri-apps/cli-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -118,8 +118,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-x64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -134,8 +134,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-x64-msvc') const bindingPackageVersion = require('@tauri-apps/cli-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -151,8 +151,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-ia32-msvc') const bindingPackageVersion = require('@tauri-apps/cli-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -167,8 +167,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-arm64-msvc') const bindingPackageVersion = require('@tauri-apps/cli-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -186,8 +186,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-darwin-universal') const bindingPackageVersion = require('@tauri-apps/cli-darwin-universal/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -202,8 +202,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-darwin-x64') const bindingPackageVersion = require('@tauri-apps/cli-darwin-x64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -218,8 +218,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-darwin-arm64') const bindingPackageVersion = require('@tauri-apps/cli-darwin-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -238,8 +238,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-freebsd-x64') const bindingPackageVersion = require('@tauri-apps/cli-freebsd-x64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -254,8 +254,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-freebsd-arm64') const bindingPackageVersion = require('@tauri-apps/cli-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -275,8 +275,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-x64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -291,8 +291,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-x64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -309,8 +309,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -325,8 +325,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -343,8 +343,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm-musleabihf') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -359,8 +359,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm-gnueabihf') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -377,8 +377,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-loong64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -393,8 +393,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-loong64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -411,8 +411,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-riscv64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -427,8 +427,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-riscv64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -444,8 +444,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-ppc64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -460,8 +460,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-s390x-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -480,8 +480,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-openharmony-arm64') const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -496,8 +496,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-openharmony-x64') const bindingPackageVersion = require('@tauri-apps/cli-openharmony-x64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -512,8 +512,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-openharmony-arm') const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -540,13 +540,17 @@ if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { wasiBindingError = err } } - if (!nativeBinding) { + if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { try { wasiBinding = require('@tauri-apps/cli-wasm32-wasi') nativeBinding = wasiBinding } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { - wasiBindingError.cause = err + if (!wasiBindingError) { + wasiBindingError = err + } else { + wasiBindingError.cause = err + } loadErrors.push(err) } } diff --git a/packages/cli/package.json b/packages/cli/package.json index d289be61f669..9720a614f0d2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/cli", - "version": "2.10.1", + "version": "2.11.1", "description": "Command line interface for building Tauri apps", "type": "commonjs", "funding": { @@ -41,7 +41,7 @@ ] }, "devDependencies": { - "@napi-rs/cli": "^3.5.1", + "@napi-rs/cli": "3.4.1", "@types/node": "^24.11.0", "cross-env": "10.1.0", "vitest": "^4.0.18" @@ -58,8 +58,9 @@ "postbuild": "node append-headers.js", "build:debug": "cross-env TARGET=node napi build --platform", "postbuild:debug": "node append-headers.js", - "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID", + "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID --skip-optional-publish", "prepack": "cp ../../crates/tauri-schema-generator/schemas/config.schema.json .", + "postpublish": "node ./postpublish.js", "version": "napi version", "test": "vitest run", "tauri": "node ./tauri.js" diff --git a/packages/cli/postpublish.js b/packages/cli/postpublish.js new file mode 100644 index 000000000000..25b978dcbfd5 --- /dev/null +++ b/packages/cli/postpublish.js @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const { execFileSync } = require('node:child_process') +const { readdirSync } = require('node:fs') +const { join } = require('node:path') + +function run(command, args, cwd = process.cwd()) { + execFileSync(command, args, { + cwd, + stdio: 'inherit', + env: process.env + }) +} + +const cliDir = process.cwd() +const npmDir = join(cliDir, 'npm') +const publishTag = process.env.npm_config_tag || 'latest' + +console.log( + `Publishing platform npm packages from postpublish hook using tag "${publishTag}"...` +) + +for (const entry of readdirSync(npmDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue + } + + const pkgDir = join(npmDir, entry.name) + run('npm', ['publish', '--tag', publishTag, '--ignore-scripts'], pkgDir) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34c0a30cfa71..6bc5043f3f2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: crates/tauri-schema-worker: devDependencies: wrangler: - specifier: ^4.70.0 - version: 4.70.0 + specifier: ^4.75.0 + version: 4.75.0 examples/api: dependencies: @@ -25,26 +25,26 @@ importers: version: link:../../packages/api/dist devDependencies: '@iconify-json/codicon': - specifier: ^1.2.47 - version: 1.2.47 + specifier: ^1.2.49 + version: 1.2.49 '@iconify-json/ph': specifier: ^1.2.2 version: 1.2.2 '@sveltejs/vite-plugin-svelte': - specifier: ^6.2.4 - version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + specifier: ^7.0.0 + version: 7.0.0(svelte@5.53.11)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) '@unocss/extractor-svelte': - specifier: ^66.6.5 - version: 66.6.5 + specifier: ^66.6.6 + version: 66.6.6 svelte: - specifier: ^5.53.7 - version: 5.53.7 + specifier: ^5.53.11 + version: 5.53.11 unocss: - specifier: ^66.6.5 - version: 66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + specifier: ^66.6.6 + version: 66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) vite: - specifier: ^7.3.1 - version: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + specifier: ^8.0.5 + version: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) examples/file-associations: {} @@ -57,10 +57,10 @@ importers: version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@rollup/plugin-terser': specifier: 1.0.0 - version: 1.0.0(rollup@4.59.0) + version: 1.0.0(rollup@4.60.3) '@rollup/plugin-typescript': specifier: 12.3.0 - version: 12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3) + version: 12.3.0(rollup@4.60.3)(tslib@2.8.1)(typescript@6.0.2) '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -83,23 +83,23 @@ importers: specifier: ^17.4.0 version: 17.4.0 rollup: - specifier: 4.59.0 - version: 4.59.0 + specifier: 4.60.3 + version: 4.60.3 tslib: specifier: ^2.8.1 version: 2.8.1 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.0 + version: 6.0.2 typescript-eslint: - specifier: ^8.56.1 - version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.58.2 + version: 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) packages/cli: devDependencies: '@napi-rs/cli': - specifier: ^3.5.1 - version: 3.5.1(@emnapi/runtime@1.8.1)(@types/node@24.11.0) + specifier: 3.4.1 + version: 3.4.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0) '@types/node': specifier: ^24.11.0 version: 24.11.0 @@ -108,7 +108,7 @@ importers: version: 10.1.0 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + version: 4.0.18(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0) packages: @@ -119,41 +119,41 @@ packages: resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.14.0': - resolution: {integrity: sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==} + '@cloudflare/unenv-preset@2.15.0': + resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} peerDependencies: unenv: 2.0.0-rc.24 - workerd: ^1.20260218.0 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 peerDependenciesMeta: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260301.1': - resolution: {integrity: sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ==} + '@cloudflare/workerd-darwin-64@1.20260317.1': + resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260301.1': - resolution: {integrity: sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ==} + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260301.1': - resolution: {integrity: sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ==} + '@cloudflare/workerd-linux-64@1.20260317.1': + resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260301.1': - resolution: {integrity: sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw==} + '@cloudflare/workerd-linux-arm64@1.20260317.1': + resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260301.1': - resolution: {integrity: sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A==} + '@cloudflare/workerd-windows-64@1.20260317.1': + resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -385,8 +385,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/codicon@1.2.47': - resolution: {integrity: sha512-9z6Of5d3w8aJAv07IT2un0SgAmFZfXSCtG4LDqsf8b/yL4vBgQ6jaCVH6Y0mDegNZzLkJ+7ieIDfbZZlIXH7CA==} + '@iconify-json/codicon@1.2.49': + resolution: {integrity: sha512-Ljl9BWw7e8xYm0l5Npnj5/aN/lEFEQDfhl7UHspPYIlZgw3ZvPOvo0gAr78aorb6X1+LZJqI0BNFAgO5r2sjIQ==} '@iconify-json/ph@1.2.2': resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==} @@ -550,134 +550,134 @@ packages: cpu: [x64] os: [win32] - '@inquirer/ansi@2.0.3': - resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} - '@inquirer/checkbox@5.1.0': - resolution: {integrity: sha512-/HjF1LN0a1h4/OFsbGKHNDtWICFU/dqXCdym719HFTyJo9IG7Otr+ziGWc9S0iQuohRZllh+WprSgd5UW5Fw0g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/confirm@6.0.8': - resolution: {integrity: sha512-Di6dgmiZ9xCSUxWUReWTqDtbhXCuG2MQm2xmgSAIruzQzBqNf49b8E07/vbCYY506kDe8BiwJbegXweG8M1klw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@11.1.5': - resolution: {integrity: sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/editor@5.0.8': - resolution: {integrity: sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/expand@5.0.8': - resolution: {integrity: sha512-QieW3F1prNw3j+hxO7/NKkG1pk3oz7pOB6+5Upwu3OIwADfPX0oZVppsqlL+Vl/uBHHDSOBY0BirLctLnXwGGg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/external-editor@2.0.3': - resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@2.0.3': - resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} - '@inquirer/input@5.0.8': - resolution: {integrity: sha512-p0IJslw0AmedLEkOU+yrEX3Aj2RTpQq7ZOf8nc1DIhjzaxRWrrgeuE5Kyh39fVRgtcACaMXx/9WNo8+GjgBOfw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/number@4.0.8': - resolution: {integrity: sha512-uGLiQah9A0F9UIvJBX52m0CnqtLaym0WpT9V4YZrjZ+YRDKZdwwoEPz06N6w8ChE2lrnsdyhY9sL+Y690Kh9gQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/password@5.0.8': - resolution: {integrity: sha512-zt1sF4lYLdvPqvmvHdmjOzuUUjuCQ897pdUCO8RbXMUDKXJTTyOQgtn23le+jwcb+MpHl3VAFvzIdxRAf6aPlA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@8.3.0': - resolution: {integrity: sha512-JAj66kjdH/F1+B7LCigjARbwstt3SNUOSzMdjpsvwJmzunK88gJeXmcm95L9nw1KynvFVuY4SzXh/3Y0lvtgSg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/rawlist@5.2.4': - resolution: {integrity: sha512-fTuJ5Cq9W286isLxwj6GGyfTjx1Zdk4qppVEPexFuA6yioCCXS4V1zfKroQqw7QdbDPN73xs2DiIAlo55+kBqg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/search@4.1.4': - resolution: {integrity: sha512-9yPTxq7LPmYjrGn3DRuaPuPbmC6u3fiWcsE9ggfLcdgO/ICHYgxq7mEy1yJ39brVvgXhtOtvDVjDh9slJxE4LQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/select@5.1.0': - resolution: {integrity: sha512-OyYbKnchS1u+zRe14LpYrN8S0wH1vD0p2yKISvSsJdH2TpI87fh4eZdWnpdbrGauCRWDph3NwxRmM4Pcm/hx1Q==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/type@4.0.3': - resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -706,12 +706,12 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@napi-rs/cli@3.5.1': - resolution: {integrity: sha512-XBfLQRDcB3qhu6bazdMJsecWW55kR85l5/k0af9BIBELXQSsCFU0fzug7PX8eQp6vVdm7W/U3z6uP5WmITB2Gw==} + '@napi-rs/cli@3.4.1': + resolution: {integrity: sha512-ayhm+NfrP5Hmh7vy5pfyYm/ktYtLh2PrgdLuqHTAubO7RoO2JkUE4F991AtgYxNewwXI8+guZLxU8itV7QnDrQ==} engines: {node: '>= 16'} hasBin: true peerDependencies: - '@emnapi/runtime': ^1.7.1 + '@emnapi/runtime': ^1.5.0 peerDependenciesMeta: '@emnapi/runtime': optional: true @@ -968,8 +968,11 @@ packages: resolution: {integrity: sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@napi-rs/wasm-tools-android-arm-eabi@1.0.1': resolution: {integrity: sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==} @@ -1250,6 +1253,9 @@ packages: '@oxc-project/types@0.115.0': resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1265,6 +1271,104 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rollup/plugin-terser@1.0.0': resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} engines: {node: '>=20.0.0'} @@ -1296,141 +1400,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} cpu: [x64] os: [win32] @@ -1449,20 +1553,12 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2': - resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + '@sveltejs/vite-plugin-svelte@7.0.0': + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: - '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 - svelte: ^5.0.0 - vite: ^6.3.0 || ^7.0.0 - - '@sveltejs/vite-plugin-svelte@6.2.4': - resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.0.0 - vite: ^6.3.0 || ^7.0.0 + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1491,137 +1587,134 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/eslint-plugin@8.56.1': - resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + '@typescript-eslint/eslint-plugin@8.58.2': + resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.56.1 + '@typescript-eslint/parser': ^8.58.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.56.1': - resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + '@typescript-eslint/parser@8.58.2': + resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.58.2': + resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + '@typescript-eslint/scope-manager@8.58.2': + resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + '@typescript-eslint/tsconfig-utils@8.58.2': + resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.56.1': - resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + '@typescript-eslint/type-utils@8.58.2': + resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + '@typescript-eslint/types@8.58.2': + resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + '@typescript-eslint/typescript-estree@8.58.2': + resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + '@typescript-eslint/utils@8.58.2': + resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + '@typescript-eslint/visitor-keys@8.58.2': + resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unocss/cli@66.6.5': - resolution: {integrity: sha512-UlETATpAZ+A5gOfj+z+BMXuIUcXCMjvlQteQE0VR2Yf0VIxz4sVO4z0VCXwXsxLTMfQiIMDpKVrGeczcYicvTA==} + '@unocss/cli@66.6.6': + resolution: {integrity: sha512-78SY8j4hAVelK+vP/adsDGaSjEITasYLFECJLHWxUJSzK+G9UIc5wtL/u4jA+zKvwVkHcDvbkcO5K6wwwpAixg==} engines: {node: '>=14'} hasBin: true - '@unocss/config@66.6.4': - resolution: {integrity: sha512-iwHl5FG81cOAMalqigjw21Z2tMa0xjN0doQxnGOLx8KP+BllruXSjBj8CRk3m6Ny9fDxfpFY0ruYbIBA5AGwDQ==} + '@unocss/config@66.6.6': + resolution: {integrity: sha512-menlnkqAFX/4wR2aandY8hSqrt01JE+rOzvtQxWaBt8kf1du62b0sS72FE5Z40n6HlEsEbF91N9FCfhnzG6i6g==} engines: {node: '>=14'} - '@unocss/core@66.6.4': - resolution: {integrity: sha512-Fii3lhVJVFrKUz6hMGAkq3sXBfNnXB2G8bldNHuBHJpDAoP1F0oO/SU/oSqSjCYvtcD5RtOn8qwzcHuuN3B/mg==} - - '@unocss/core@66.6.5': - resolution: {integrity: sha512-hzjo+0EF+pNbf+tb0OjRNZRF9BJoKECcZZgtufxRPpWJdlv+aYmNkH1p9fldlHHzYcn3ZqVnnHnmk7HwaolJbg==} + '@unocss/core@66.6.6': + resolution: {integrity: sha512-Sbbx0ZQqmV8K2lg8E+z9MJzWb1MgRtJnvqzxDIrNuBjXasKhbcFt5wEMBtEZJOr63Z4ck0xThhZK53HmYT2jmg==} - '@unocss/extractor-arbitrary-variants@66.6.5': - resolution: {integrity: sha512-wqzRtbyy3I595WCwwb8VBmznJTHWcTdylzVT+WBgacJDjRlT1sXaq2fRlOsHvtTRj1qG70t3PwKc6XgU0hutNg==} + '@unocss/extractor-arbitrary-variants@66.6.6': + resolution: {integrity: sha512-uMzekF2miZRUwSZGvy3yYQiBAcSAs9LiXK8e3NjldxEw8xcRDWgTErxgStRoBeAD6UyzDcg/Cvwtf2guMbtR+g==} - '@unocss/extractor-svelte@66.6.5': - resolution: {integrity: sha512-Zg58V3L5jxSPzT2BEZY87+WH0gVMI4VKZuwNwFS3ht83RsCD32eGjJxrk6w5CuIYDV6MeGSkosyPIwLY+OFD3w==} + '@unocss/extractor-svelte@66.6.6': + resolution: {integrity: sha512-5+Et3jiSFlMqxkoyVLsoT2/Rd8x/Jd65i5KzIyXMtQccDmqN2wSXuyvB2h5sLauHn4bBe/qOWO3PfGjbXBGWOA==} - '@unocss/inspector@66.6.5': - resolution: {integrity: sha512-rrXPlSeRfYajEL65FL1Ok9Hfhjy9zvuZZwqXh9P0qCJlou2r2IqDFO/Gf9j5yO89tnKIfJ8ff6jEyqUmzbKSMQ==} + '@unocss/inspector@66.6.6': + resolution: {integrity: sha512-CpXIsqHwxCXJtUjUz6S29diHCIA+EJ1u5WML/6m2YPI4ObgWAVKrExy09inSg2icS52lFkWWdWQSeqc9kl5W6Q==} - '@unocss/preset-attributify@66.6.5': - resolution: {integrity: sha512-fx+pKMZ0WgT+dfinVaLkNXlx6oZFwtMbZj5O/1SQia0UcfhnyS+G35HYpbgoc9GEAl3DclxxotzZjveZm++9fA==} + '@unocss/preset-attributify@66.6.6': + resolution: {integrity: sha512-3H12UI1rBt60PQy+S4IEeFYWu1/WQFuc2yhJ5mu/RCvX5/qwlIGanBpuh+xzTPXU1fWBlZN68yyO9uWOQgTqZQ==} - '@unocss/preset-icons@66.6.5': - resolution: {integrity: sha512-03ppAcTWD77w1WZhORT8c9beTHBtWu3cx+c4qfShOfY6LQmZgx5i7DhCij5Wcj/U1zYA4Vrh13CDEmpsdZO3Cw==} + '@unocss/preset-icons@66.6.6': + resolution: {integrity: sha512-HfIEEqf3jyKexOB2Sux556n0NkPoUftb2H4+Cf7prJvKHopMkZ/OUkXjwvUlxt1e5UpAEaIa0A2Ir7+ApxXoGA==} - '@unocss/preset-mini@66.6.5': - resolution: {integrity: sha512-Ber3k2jlE8JP0y507hw/lvdDvcxfY0t4zaGA7hVZdEqlH6Eus/TqIVZ9tdMH4u0VDWYeAs98YV+auUJmMqGXpg==} + '@unocss/preset-mini@66.6.6': + resolution: {integrity: sha512-k+/95PKMPOK57cJcSmz34VkIFem8BlujRRx6/L0Yusw7vLJMh98k0rPhC5s+NomZ/d9ZPgbNylskLhItJlak3w==} - '@unocss/preset-tagify@66.6.5': - resolution: {integrity: sha512-YYk/eg1OWX4Nx7rK1YZLMHXXntzNRDHp6BIInJteQmlXw0sFgrtdMKj7fnxrORsBDHwxWMp4sWEucPvfCtTlVQ==} + '@unocss/preset-tagify@66.6.6': + resolution: {integrity: sha512-KgBXYPYS0g4TVC3NLiIB78YIqUlvDLanz1EHIDo34rOTUfMgY8Uf5VuDJAzMu4Sc0LiwwBJbk6nIG9/Zm7ufWg==} - '@unocss/preset-typography@66.6.5': - resolution: {integrity: sha512-Cb63tdC0P2rgj/4t4DrSCl6RHebNpjUp9FQArg0KCnFnW75nWtKlsKpHuEXpi7KwrgOIx+rjlkwC1bDcsdNLHw==} + '@unocss/preset-typography@66.6.6': + resolution: {integrity: sha512-SM1km5nqt15z4sTabfOobSC633I5Ol5nnme6JFTra4wiyCUNs+Cg31nJ6jnopWDUT4SEAXqfUH7jKSSoCnI6ZA==} - '@unocss/preset-uno@66.6.5': - resolution: {integrity: sha512-feZfGyzt3dH4h6yP2kjsx5MuoI1gU7vY/VL5O+ObosaB7HzzOFCsu2WzlvWn/FTRBi+scvdq436hsfflVyHYfQ==} + '@unocss/preset-uno@66.6.6': + resolution: {integrity: sha512-40PcBDtlhW7QP7e/WOxC684IhN5T1dXvj1dgx9ZzK+8lEDGjcX7bN2noW4aSenzSrHymeSsMrL/0ltL4ED/5Zw==} - '@unocss/preset-web-fonts@66.6.5': - resolution: {integrity: sha512-u5jEHYTMeseykqinXd2VY2n7q9yFQlZotREpfSAft8ENNJdV7Yg/6It3lL68zT/k1AV/A8gk94KEuDh0fnoSxQ==} + '@unocss/preset-web-fonts@66.6.6': + resolution: {integrity: sha512-5ikwgrJB8VPzKd0bqgGNgYUGix90KFnVtKJPjWTP5qsv3+ZtZnea1rRbAFl8i2t52hg35msNBsQo+40IC3xB6A==} - '@unocss/preset-wind3@66.6.5': - resolution: {integrity: sha512-0ccQoJmHq4tTnn5C0UKhP598B/gG65AjqlfgfRpwt059yAWYqizGy6MRUGdLklyEK4H06E6qbMBqIjla2rOexQ==} + '@unocss/preset-wind3@66.6.6': + resolution: {integrity: sha512-rk6gPPIQ7z2DVucOqp7XZ4vGpKAuzBV1vtUDvDh5WscxzO/QlqaeTfTALk5YgGpmLaF4+ns6FrTgLjV+wHgHuQ==} - '@unocss/preset-wind4@66.6.5': - resolution: {integrity: sha512-JT57CU60PY3/PHBvxY+UG53I9K+awin/TodZTn4lqQNnF2v6fjkeBKiys9cxeoP4wbHuQWorrW4GqRLNDWIMcw==} + '@unocss/preset-wind4@66.6.6': + resolution: {integrity: sha512-caTDM9rZSlp4tyPWWAnwMvQr2PXq53LsEYwd3N8zj0ou2hcsqptJvF+mFvyhvGF66x26wWJr/FwuUEhh7qycaw==} - '@unocss/preset-wind@66.6.5': - resolution: {integrity: sha512-GLu7LzVF0LHqdZoHFZ8dbsCv8TD5ZH/r10CQbrL5qwmp4a/uyfDEmsre4Nsqim7JktRyXn3HK2XQmTB8AmXpgQ==} + '@unocss/preset-wind@66.6.6': + resolution: {integrity: sha512-TMy3lZ35FP/4QqDHOLWZmV+RoOGWUDqnDEOTjOKI1CQARGta0ppUmq+IZMuI1ZJLuOa4OZ9V6SfnwMXwRLgXmw==} - '@unocss/rule-utils@66.6.5': - resolution: {integrity: sha512-eDGXoMebb5aeEAFa2y4gnGLC+CHZPx93JYCt6uvEyf9xOoetwDcZaYC8brWdjaSKn+WVgsfxiZreC7F0rJywOQ==} + '@unocss/rule-utils@66.6.6': + resolution: {integrity: sha512-krWtQKGshOaqQMuxeGq1NOA8NL35VdpYlmQEWOe39BY6TACT51bgQFu40MRfsAIMZZtoGS2YYTrnHojgR92omw==} engines: {node: '>=14'} - '@unocss/transformer-attributify-jsx@66.6.5': - resolution: {integrity: sha512-/dVaRR7V/2Alskb2rUPmP/lhyb/YCxYyYNxp30kxxW0ew6mZWXQRzsxOJJVmGp23Uw7HxUW63t8zXzUdoI0b+g==} + '@unocss/transformer-attributify-jsx@66.6.6': + resolution: {integrity: sha512-NnDchmN2EeFLy4lfVqDgNe9j1+w2RLL2L9zKECXs5g6rDVfeeEK6FNgxSq3XnPcKltjNCy1pF4MaDOROG7r8yA==} - '@unocss/transformer-compile-class@66.6.5': - resolution: {integrity: sha512-U/ukk5lyZOFNyz9hVzZBkxciayjgimyfPuQBa5PHSC4W3nDmnFd1zgXzUVaM6KduPmiTExzpJSDgELb2OTbpqg==} + '@unocss/transformer-compile-class@66.6.6': + resolution: {integrity: sha512-KKssJxU8fZ9x84yznIirbtta2sB0LN/3lm0bp+Wl1298HITaNiVeG2n26iStQ3N7r240xRN2RarxncSVCMFwWw==} - '@unocss/transformer-directives@66.6.5': - resolution: {integrity: sha512-QgofDdDedNK6dQ246+RXhM6gTzRz7NuetQQ8UnNgArm4PBHngVrrkjCzG1ByDTtEtoE8WR70UMR4Vf5dXTcHPw==} + '@unocss/transformer-directives@66.6.6': + resolution: {integrity: sha512-CReFTcBfMtKkRvzIqxL20VptWt5C1Om27dwoKzyVFBXv0jzViWysbu0y0AQg3bsgD4cFqndFyAGyeL84j0nbKg==} - '@unocss/transformer-variant-group@66.6.5': - resolution: {integrity: sha512-k6vQgn/P7ObHBRYw6o1+xwdQIfwc6b9O5TFFe87UmBB6hJ2zaHWRVuPB6oky7F9Gz8bPfXC3WJuv7UyIwRmBQQ==} + '@unocss/transformer-variant-group@66.6.6': + resolution: {integrity: sha512-j4L/0Tw6AdMVB2dDnuBlDbevyL1/0CAk88a77VF/VjgEIBwB9VXsCCUsxz+2Dohcl7N2GMm7+kpaWA6qt2PSaA==} - '@unocss/vite@66.6.5': - resolution: {integrity: sha512-J/QZa6h94ordZlZytIKQkuYa+G2GiWiS3y9O1uoHAAN2tzFSkgCXNUif7lHu1h4eCrgC0AOHJSYWg1LIASNDkg==} + '@unocss/vite@66.6.6': + resolution: {integrity: sha512-DgG7KcUUMtoDhPOlFf2l4dR+66xZ23SdZvTYpikk5nZfLCzZd62vedutD7x0bTR6VpK2YRq39B+F+Z6TktNY/w==} peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 @@ -1667,6 +1760,14 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1692,8 +1793,8 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1731,6 +1832,13 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1777,8 +1885,8 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.6: + resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -1787,8 +1895,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.3: - resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + devalue@5.6.4: + resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -1801,6 +1909,9 @@ packages: node-addon-api: optional: true + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -1903,15 +2014,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1940,8 +2042,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1995,6 +2097,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2040,6 +2146,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -2064,13 +2244,13 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - miniflare@4.20260301.1: - resolution: {integrity: sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog==} + miniflare@4.20260317.0: + resolution: {integrity: sha512-xuwk5Kjv+shi5iUBAdCrRl9IaWSGnTU8WuTQzsUS2GlSDIMCJuu8DiF/d9ExjMXYiQG5ml+k9SVKnMj8cRkq0w==} engines: {node: '>=18.0.0'} hasBin: true - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} mlly@1.8.0: @@ -2083,9 +2263,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -2151,19 +2331,19 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -2202,8 +2382,13 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2221,8 +2406,8 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@7.0.4: - resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==} + serialize-javascript@7.0.5: + resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} sharp@0.34.5: @@ -2269,6 +2454,14 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -2277,8 +2470,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte@5.53.7: - resolution: {integrity: sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==} + svelte@5.53.11: + resolution: {integrity: sha512-GYmqRjRhJYLQBonfdfGAt28gkfWEShrtXKGXcFGneXi502aBE+I1dJcs/YQriByvP6xqXRz/OdBGC6tfvUQHyQ==} engines: {node: '>=18'} terser@5.46.0: @@ -2293,8 +2486,8 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tinyrainbow@3.0.3: @@ -2309,8 +2502,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -2328,15 +2521,15 @@ packages: type-level-regexp@0.1.17: resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} - typescript-eslint@8.56.1: - resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + typescript-eslint@8.58.2: + resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -2352,8 +2545,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -2362,13 +2555,13 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - unocss@66.6.5: - resolution: {integrity: sha512-WlpPlV7yAzEPREcwaKeacP+1jOm6ImhyKJRkK18tIW2b2BRZZDKln7X8P+NzJtAr0kziNY/ttUKZNZRnSmzP1A==} + unocss@66.6.6: + resolution: {integrity: sha512-PRKK945e2oZKHV664MA5Z9CDHbvY/V79IvTOUWKZ514jpl3UsJU3sS+skgxmKJSmwrWvXE5OVcmPthJrD/7vxg==} engines: {node: '>=14'} peerDependencies: - '@unocss/astro': 66.6.5 - '@unocss/postcss': 66.6.5 - '@unocss/webpack': 66.6.5 + '@unocss/astro': 66.6.6 + '@unocss/postcss': 66.6.6 + '@unocss/webpack': 66.6.6 peerDependenciesMeta: '@unocss/astro': optional: true @@ -2388,8 +2581,8 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2428,6 +2621,49 @@ packages: yaml: optional: true + vite@8.0.5: + resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.2: resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} peerDependencies: @@ -2487,21 +2723,25 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260301.1: - resolution: {integrity: sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw==} + workerd@1.20260317.1: + resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==} engines: {node: '>=16'} hasBin: true - wrangler@4.70.0: - resolution: {integrity: sha512-PNDZ9o4e+B5x+1bUbz62Hmwz6G9lw+I9pnYe/AguLddJFjfIyt2cmFOUOb3eOZSoXsrhcEPUg2YidYIbVwUkfw==} + wrangler@4.75.0: + resolution: {integrity: sha512-Efk1tcnm4eduBYpH1sSjMYydXMnIFPns/qABI3+fsbDrUk5GksNYX8nYGVP4sFygvGPO7kJc36YJKB5ooA7JAg==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260226.1 + '@cloudflare/workers-types': ^4.20260317.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -2518,6 +2758,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -2536,25 +2780,25 @@ snapshots: '@cloudflare/kv-asset-handler@0.4.2': {} - '@cloudflare/unenv-preset@2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1)': + '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260301.1 + workerd: 1.20260317.1 - '@cloudflare/workerd-darwin-64@1.20260301.1': + '@cloudflare/workerd-darwin-64@1.20260317.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260301.1': + '@cloudflare/workerd-darwin-arm64@1.20260317.1': optional: true - '@cloudflare/workerd-linux-64@1.20260301.1': + '@cloudflare/workerd-linux-64@1.20260317.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260301.1': + '@cloudflare/workerd-linux-arm64@1.20260317.1': optional: true - '@cloudflare/workerd-windows-64@1.20260301.1': + '@cloudflare/workerd-windows-64@1.20260317.1': optional: true '@cspotcode/source-map-support@0.8.1': @@ -2668,7 +2912,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.2 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -2702,7 +2946,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/codicon@1.2.47': + '@iconify-json/codicon@1.2.49': dependencies: '@iconify/types': 2.0.0 @@ -2814,122 +3058,128 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/ansi@2.0.3': {} + '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@5.1.0(@types/node@24.11.0)': + '@inquirer/checkbox@4.3.2(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/confirm@6.0.8(@types/node@24.11.0)': + '@inquirer/confirm@5.1.21(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/core@11.1.5(@types/node@24.11.0)': + '@inquirer/core@10.3.2(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) cli-width: 4.1.0 - fast-wrap-ansi: 0.2.0 - mute-stream: 3.0.0 + mute-stream: 2.0.0 signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/editor@5.0.8(@types/node@24.11.0)': + '@inquirer/editor@4.2.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/external-editor': 2.0.3(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/external-editor': 1.0.3(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/expand@5.0.8(@types/node@24.11.0)': + '@inquirer/expand@4.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/external-editor@2.0.3(@types/node@24.11.0)': + '@inquirer/external-editor@1.0.3(@types/node@24.11.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/figures@2.0.3': {} + '@inquirer/figures@1.0.15': {} - '@inquirer/input@5.0.8(@types/node@24.11.0)': + '@inquirer/input@4.3.1(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/number@4.0.8(@types/node@24.11.0)': + '@inquirer/number@3.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/password@5.0.8(@types/node@24.11.0)': + '@inquirer/password@4.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/prompts@8.3.0(@types/node@24.11.0)': - dependencies: - '@inquirer/checkbox': 5.1.0(@types/node@24.11.0) - '@inquirer/confirm': 6.0.8(@types/node@24.11.0) - '@inquirer/editor': 5.0.8(@types/node@24.11.0) - '@inquirer/expand': 5.0.8(@types/node@24.11.0) - '@inquirer/input': 5.0.8(@types/node@24.11.0) - '@inquirer/number': 4.0.8(@types/node@24.11.0) - '@inquirer/password': 5.0.8(@types/node@24.11.0) - '@inquirer/rawlist': 5.2.4(@types/node@24.11.0) - '@inquirer/search': 4.1.4(@types/node@24.11.0) - '@inquirer/select': 5.1.0(@types/node@24.11.0) + '@inquirer/prompts@7.10.1(@types/node@24.11.0)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.11.0) + '@inquirer/confirm': 5.1.21(@types/node@24.11.0) + '@inquirer/editor': 4.2.23(@types/node@24.11.0) + '@inquirer/expand': 4.0.23(@types/node@24.11.0) + '@inquirer/input': 4.3.1(@types/node@24.11.0) + '@inquirer/number': 3.0.23(@types/node@24.11.0) + '@inquirer/password': 4.0.23(@types/node@24.11.0) + '@inquirer/rawlist': 4.1.11(@types/node@24.11.0) + '@inquirer/search': 3.2.2(@types/node@24.11.0) + '@inquirer/select': 4.4.2(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/rawlist@5.2.4(@types/node@24.11.0)': + '@inquirer/rawlist@4.1.11(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/search@4.1.4(@types/node@24.11.0)': + '@inquirer/search@3.2.2(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/select@5.1.0(@types/node@24.11.0)': + '@inquirer/select@4.4.2(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/type@4.0.3(@types/node@24.11.0)': + '@inquirer/type@3.0.10(@types/node@24.11.0)': optionalDependencies: '@types/node': 24.11.0 @@ -2962,23 +3212,24 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/cli@3.5.1(@emnapi/runtime@1.8.1)(@types/node@24.11.0)': + '@napi-rs/cli@3.4.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)': dependencies: - '@inquirer/prompts': 8.3.0(@types/node@24.11.0) - '@napi-rs/cross-toolchain': 1.0.3 - '@napi-rs/wasm-tools': 1.0.1 + '@inquirer/prompts': 7.10.1(@types/node@24.11.0) + '@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@napi-rs/wasm-tools': 1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@octokit/rest': 22.0.1 clipanion: 4.0.0-rc.4(typanion@3.14.0) colorette: 2.0.20 + debug: 4.4.3 emnapi: 1.8.1 es-toolkit: 1.45.1 js-yaml: 4.1.1 - obug: 2.1.1 semver: 7.7.4 typanion: 3.14.0 optionalDependencies: '@emnapi/runtime': 1.8.1 transitivePeerDependencies: + - '@emnapi/core' - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' - '@napi-rs/cross-toolchain-arm64-target-ppc64le' @@ -2993,12 +3244,14 @@ snapshots: - node-addon-api - supports-color - '@napi-rs/cross-toolchain@1.0.3': + '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/lzma': 1.4.5 - '@napi-rs/tar': 1.1.0 + '@napi-rs/lzma': 1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@napi-rs/tar': 1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) debug: 4.4.3 transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - supports-color '@napi-rs/lzma-android-arm-eabi@1.4.5': @@ -3040,9 +3293,12 @@ snapshots: '@napi-rs/lzma-linux-x64-musl@1.4.5': optional: true - '@napi-rs/lzma-wasm32-wasi@1.4.5': + '@napi-rs/lzma-wasm32-wasi@1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@napi-rs/lzma-win32-arm64-msvc@1.4.5': @@ -3054,7 +3310,7 @@ snapshots: '@napi-rs/lzma-win32-x64-msvc@1.4.5': optional: true - '@napi-rs/lzma@1.4.5': + '@napi-rs/lzma@1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': optionalDependencies: '@napi-rs/lzma-android-arm-eabi': 1.4.5 '@napi-rs/lzma-android-arm64': 1.4.5 @@ -3069,10 +3325,13 @@ snapshots: '@napi-rs/lzma-linux-s390x-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-musl': 1.4.5 - '@napi-rs/lzma-wasm32-wasi': 1.4.5 + '@napi-rs/lzma-wasm32-wasi': 1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@napi-rs/lzma-win32-arm64-msvc': 1.4.5 '@napi-rs/lzma-win32-ia32-msvc': 1.4.5 '@napi-rs/lzma-win32-x64-msvc': 1.4.5 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' '@napi-rs/tar-android-arm-eabi@1.1.0': optional: true @@ -3110,9 +3369,12 @@ snapshots: '@napi-rs/tar-linux-x64-musl@1.1.0': optional: true - '@napi-rs/tar-wasm32-wasi@1.1.0': + '@napi-rs/tar-wasm32-wasi@1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@napi-rs/tar-win32-arm64-msvc@1.1.0': @@ -3124,7 +3386,7 @@ snapshots: '@napi-rs/tar-win32-x64-msvc@1.1.0': optional: true - '@napi-rs/tar@1.1.0': + '@napi-rs/tar@1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': optionalDependencies: '@napi-rs/tar-android-arm-eabi': 1.1.0 '@napi-rs/tar-android-arm64': 1.1.0 @@ -3138,12 +3400,15 @@ snapshots: '@napi-rs/tar-linux-s390x-gnu': 1.1.0 '@napi-rs/tar-linux-x64-gnu': 1.1.0 '@napi-rs/tar-linux-x64-musl': 1.1.0 - '@napi-rs/tar-wasm32-wasi': 1.1.0 + '@napi-rs/tar-wasm32-wasi': 1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@napi-rs/tar-win32-arm64-msvc': 1.1.0 '@napi-rs/tar-win32-ia32-msvc': 1.1.0 '@napi-rs/tar-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: '@emnapi/core': 1.8.1 '@emnapi/runtime': 1.8.1 @@ -3177,9 +3442,12 @@ snapshots: '@napi-rs/wasm-tools-linux-x64-musl@1.0.1': optional: true - '@napi-rs/wasm-tools-wasm32-wasi@1.0.1': + '@napi-rs/wasm-tools-wasm32-wasi@1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1': @@ -3191,7 +3459,7 @@ snapshots: '@napi-rs/wasm-tools-win32-x64-msvc@1.0.1': optional: true - '@napi-rs/wasm-tools@1.0.1': + '@napi-rs/wasm-tools@1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': optionalDependencies: '@napi-rs/wasm-tools-android-arm-eabi': 1.0.1 '@napi-rs/wasm-tools-android-arm64': 1.0.1 @@ -3202,10 +3470,13 @@ snapshots: '@napi-rs/wasm-tools-linux-arm64-musl': 1.0.1 '@napi-rs/wasm-tools-linux-x64-gnu': 1.0.1 '@napi-rs/wasm-tools-linux-x64-musl': 1.0.1 - '@napi-rs/wasm-tools-wasm32-wasi': 1.0.1 + '@napi-rs/wasm-tools-wasm32-wasi': 1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@napi-rs/wasm-tools-win32-arm64-msvc': 1.0.1 '@napi-rs/wasm-tools-win32-ia32-msvc': 1.0.1 '@napi-rs/wasm-tools-win32-x64-msvc': 1.0.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' '@nodelib/fs.scandir@2.1.5': dependencies: @@ -3330,9 +3601,12 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.115.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.115.0': + '@oxc-parser/binding-wasm32-wasi@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@oxc-parser/binding-win32-arm64-msvc@0.115.0': @@ -3346,6 +3620,8 @@ snapshots: '@oxc-project/types@0.115.0': {} + '@oxc-project/types@0.122.0': {} + '@polka/url@1.0.0-next.29': {} '@poppinss/colors@4.1.6': @@ -3364,104 +3640,156 @@ snapshots: dependencies: quansync: 1.0.0 - '@rollup/plugin-terser@1.0.0(rollup@4.59.0)': + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rollup/plugin-terser@1.0.0(rollup@4.60.3)': dependencies: - serialize-javascript: 7.0.4 + serialize-javascript: 7.0.5 smob: 1.6.1 terser: 5.46.0 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.3 - '@rollup/plugin-typescript@12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3)': + '@rollup/plugin-typescript@12.3.0(rollup@4.60.3)(tslib@2.8.1)(typescript@6.0.2)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) resolve: 1.22.11 - typescript: 5.9.3 + typescript: 6.0.2 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.3 tslib: 2.8.1 - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + '@rollup/pluginutils@5.3.0(rollup@4.60.3)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.3 - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/rollup-android-arm-eabi@4.60.3': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.60.3': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.60.3': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.3': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.60.3': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.60.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.60.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.60.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.60.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.60.3': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.3': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.60.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.60.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.60.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.3': optional: true '@sindresorhus/is@7.2.0': {} @@ -3474,22 +3802,14 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) - obug: 2.1.1 - svelte: 5.53.7 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) - - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.53.7 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) - vitefu: 1.1.2(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + svelte: 5.53.11 + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) + vitefu: 1.1.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) '@tybys/wasm-util@0.10.1': dependencies: @@ -3520,105 +3840,105 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2))(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/parser': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/type-utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.2 eslint: 10.0.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/scope-manager@8.58.2': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': dependencies: - typescript: 5.9.3 + typescript: 6.0.2 - '@typescript-eslint/type-utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) debug: 4.4.3 eslint: 10.0.2(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.58.2': {} - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.56.1': + '@typescript-eslint/visitor-keys@8.58.2': dependencies: - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/types': 8.58.2 eslint-visitor-keys: 5.0.1 - '@unocss/cli@66.6.5': + '@unocss/cli@66.6.6': dependencies: '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.6.4 - '@unocss/core': 66.6.5 - '@unocss/preset-wind3': 66.6.5 - '@unocss/preset-wind4': 66.6.5 - '@unocss/transformer-directives': 66.6.5 + '@unocss/config': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + '@unocss/preset-wind4': 66.6.6 + '@unocss/transformer-directives': 66.6.6 cac: 6.7.14 chokidar: 5.0.0 colorette: 2.0.20 @@ -3626,123 +3946,124 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 perfect-debounce: 2.1.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 unplugin-utils: 0.3.1 - '@unocss/config@66.6.4': + '@unocss/config@66.6.6': dependencies: - '@unocss/core': 66.6.4 + '@unocss/core': 66.6.6 colorette: 2.0.20 consola: 3.4.2 unconfig: 7.5.0 - '@unocss/core@66.6.4': {} + '@unocss/core@66.6.6': {} - '@unocss/core@66.6.5': {} - - '@unocss/extractor-arbitrary-variants@66.6.5': + '@unocss/extractor-arbitrary-variants@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/extractor-svelte@66.6.5': {} + '@unocss/extractor-svelte@66.6.6': {} - '@unocss/inspector@66.6.5': + '@unocss/inspector@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 colorette: 2.0.20 gzip-size: 6.0.0 sirv: 3.0.2 - '@unocss/preset-attributify@66.6.5': + '@unocss/preset-attributify@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/preset-icons@66.6.5': + '@unocss/preset-icons@66.6.6': dependencies: '@iconify/utils': 3.1.0 - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 ofetch: 1.5.1 - '@unocss/preset-mini@66.6.5': + '@unocss/preset-mini@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/extractor-arbitrary-variants': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/extractor-arbitrary-variants': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-tagify@66.6.5': + '@unocss/preset-tagify@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/preset-typography@66.6.5': + '@unocss/preset-typography@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-uno@66.6.5': + '@unocss/preset-uno@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/preset-wind3': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 - '@unocss/preset-web-fonts@66.6.5': + '@unocss/preset-web-fonts@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 ofetch: 1.5.1 - '@unocss/preset-wind3@66.6.5': + '@unocss/preset-wind3@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/preset-mini': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/preset-mini': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-wind4@66.6.5': + '@unocss/preset-wind4@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/extractor-arbitrary-variants': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/extractor-arbitrary-variants': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-wind@66.6.5': + '@unocss/preset-wind@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/preset-wind3': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 - '@unocss/rule-utils@66.6.5': + '@unocss/rule-utils@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 magic-string: 0.30.21 - '@unocss/transformer-attributify-jsx@66.6.5': + '@unocss/transformer-attributify-jsx@66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@unocss/core': 66.6.5 - oxc-parser: 0.115.0 - oxc-walker: 0.7.0(oxc-parser@0.115.0) + '@unocss/core': 66.6.6 + oxc-parser: 0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + oxc-walker: 0.7.0(oxc-parser@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - '@unocss/transformer-compile-class@66.6.5': + '@unocss/transformer-compile-class@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/transformer-directives@66.6.5': + '@unocss/transformer-directives@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 css-tree: 3.1.0 - '@unocss/transformer-variant-group@66.6.5': + '@unocss/transformer-variant-group@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/vite@66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': + '@unocss/vite@66.6.6(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0))': dependencies: '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.6.4 - '@unocss/core': 66.6.5 - '@unocss/inspector': 66.6.5 + '@unocss/config': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/inspector': 66.6.6 chokidar: 5.0.0 magic-string: 0.30.21 pathe: 2.0.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 unplugin-utils: 0.3.1 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) '@vitest/expect@4.0.18': dependencies: @@ -3753,13 +4074,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': + '@vitest/mocker@4.0.18(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -3796,6 +4117,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + argparse@2.0.1: {} aria-query@5.3.1: {} @@ -3810,7 +4137,7 @@ snapshots: blake3-wasm@2.1.5: {} - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -3838,6 +4165,12 @@ snapshots: clsx@2.1.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + colorette@2.0.20: {} commander@2.20.3: {} @@ -3872,18 +4205,20 @@ snapshots: deepmerge@4.3.1: {} - defu@6.1.4: {} + defu@6.1.6: {} destr@2.0.5: {} detect-libc@2.1.2: {} - devalue@5.6.3: {} + devalue@5.6.4: {} duplexer@0.1.2: {} emnapi@1.8.1: {} + emoji-regex@8.0.0: {} + error-stack-parser-es@1.0.5: {} es-module-lexer@1.7.0: {} @@ -3969,7 +4304,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -4025,23 +4360,13 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} - - fast-string-width@3.0.2: - dependencies: - fast-string-truncated-width: 3.0.3 - - fast-wrap-ansi@0.2.0: - dependencies: - fast-string-width: 3.0.2 - fastq@1.20.1: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -4058,10 +4383,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.4: {} + flatted@3.4.2: {} fsevents@2.3.3: optional: true @@ -4102,6 +4427,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4139,6 +4466,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + locate-character@3.0.0: {} locate-path@6.0.0: @@ -4166,23 +4542,23 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 - miniflare@4.20260301.1: + miniflare@4.20260317.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.18.2 - workerd: 1.20260301.1 + undici: 7.24.4 + workerd: 1.20260317.1 ws: 8.18.0 youch: 4.1.0-beta.10 transitivePeerDependencies: - bufferutil - utf-8-validate - minimatch@10.2.4: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 mlly@1.8.0: dependencies: @@ -4195,7 +4571,7 @@ snapshots: ms@2.1.3: {} - mute-stream@3.0.0: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -4220,7 +4596,7 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.115.0: + oxc-parser@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1): dependencies: '@oxc-project/types': 0.115.0 optionalDependencies: @@ -4240,15 +4616,18 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu': 0.115.0 '@oxc-parser/binding-linux-x64-musl': 0.115.0 '@oxc-parser/binding-openharmony-arm64': 0.115.0 - '@oxc-parser/binding-wasm32-wasi': 0.115.0 + '@oxc-parser/binding-wasm32-wasi': 0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@oxc-parser/binding-win32-arm64-msvc': 0.115.0 '@oxc-parser/binding-win32-ia32-msvc': 0.115.0 '@oxc-parser/binding-win32-x64-msvc': 0.115.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - oxc-walker@0.7.0(oxc-parser@0.115.0): + oxc-walker@0.7.0(oxc-parser@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)): dependencies: magic-regexp: 0.10.0 - oxc-parser: 0.115.0 + oxc-parser: 0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) p-limit@3.1.0: dependencies: @@ -4274,9 +4653,9 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pkg-types@1.3.1: dependencies: @@ -4284,7 +4663,7 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - postcss@8.5.8: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -4312,35 +4691,59 @@ snapshots: reusify@1.1.0: {} - rollup@4.59.0: + rolldown@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + rollup@4.60.3: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 run-parallel@1.2.0: @@ -4355,7 +4758,7 @@ snapshots: semver@7.7.4: {} - serialize-javascript@7.0.4: {} + serialize-javascript@7.0.5: {} sharp@0.34.5: dependencies: @@ -4419,11 +4822,21 @@ snapshots: std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + supports-color@10.2.2: {} supports-preserve-symlinks-flag@1.0.0: {} - svelte@5.53.7: + svelte@5.53.11: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -4434,7 +4847,7 @@ snapshots: aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.3 + devalue: 5.6.4 esm-env: 1.2.2 esrap: 2.2.3 is-reference: 3.0.3 @@ -4453,10 +4866,10 @@ snapshots: tinyexec@1.0.2: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyrainbow@3.0.3: {} @@ -4466,9 +4879,9 @@ snapshots: totalist@3.0.1: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 tslib@2.8.1: {} @@ -4480,18 +4893,18 @@ snapshots: type-level-regexp@0.1.17: {} - typescript-eslint@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2))(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@6.0.2: {} ufo@1.6.3: {} @@ -4503,14 +4916,14 @@ snapshots: unconfig@7.5.0: dependencies: '@quansync/fs': 1.0.0 - defu: 6.1.4 + defu: 6.1.6 jiti: 2.6.1 quansync: 1.0.0 unconfig-core: 7.5.0 undici-types@7.16.0: {} - undici@7.18.2: {} + undici@7.24.4: {} unenv@2.0.0-rc.24: dependencies: @@ -4518,66 +4931,86 @@ snapshots: universal-user-agent@7.0.3: {} - unocss@66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)): - dependencies: - '@unocss/cli': 66.6.5 - '@unocss/core': 66.6.5 - '@unocss/preset-attributify': 66.6.5 - '@unocss/preset-icons': 66.6.5 - '@unocss/preset-mini': 66.6.5 - '@unocss/preset-tagify': 66.6.5 - '@unocss/preset-typography': 66.6.5 - '@unocss/preset-uno': 66.6.5 - '@unocss/preset-web-fonts': 66.6.5 - '@unocss/preset-wind': 66.6.5 - '@unocss/preset-wind3': 66.6.5 - '@unocss/preset-wind4': 66.6.5 - '@unocss/transformer-attributify-jsx': 66.6.5 - '@unocss/transformer-compile-class': 66.6.5 - '@unocss/transformer-directives': 66.6.5 - '@unocss/transformer-variant-group': 66.6.5 - '@unocss/vite': 66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + unocss@66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)): + dependencies: + '@unocss/cli': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/preset-attributify': 66.6.6 + '@unocss/preset-icons': 66.6.6 + '@unocss/preset-mini': 66.6.6 + '@unocss/preset-tagify': 66.6.6 + '@unocss/preset-typography': 66.6.6 + '@unocss/preset-uno': 66.6.6 + '@unocss/preset-web-fonts': 66.6.6 + '@unocss/preset-wind': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + '@unocss/preset-wind4': 66.6.6 + '@unocss/transformer-attributify-jsx': 66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@unocss/transformer-compile-class': 66.6.6 + '@unocss/transformer-directives': 66.6.6 + '@unocss/transformer-variant-group': 66.6.6 + '@unocss/vite': 66.6.6(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - vite unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0): + vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0): dependencies: esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.12 + rollup: 4.60.3 + tinyglobby: 0.2.16 optionalDependencies: '@types/node': 24.11.0 fsevents: 2.3.3 jiti: 2.6.1 + lightningcss: 1.32.0 terser: 5.46.0 - vitefu@1.1.2(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)): + vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.12 + rolldown: 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + tinyglobby: 0.2.16 optionalDependencies: - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + '@types/node': 24.11.0 + esbuild: 0.27.3 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.46.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - vitest@4.0.18(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0): + vitefu@1.1.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)): + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) + + vitest@4.0.18(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + '@vitest/mocker': 4.0.18(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -4588,13 +5021,13 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 1.0.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.11.0 @@ -4624,34 +5057,42 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260301.1: + workerd@1.20260317.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260301.1 - '@cloudflare/workerd-darwin-arm64': 1.20260301.1 - '@cloudflare/workerd-linux-64': 1.20260301.1 - '@cloudflare/workerd-linux-arm64': 1.20260301.1 - '@cloudflare/workerd-windows-64': 1.20260301.1 + '@cloudflare/workerd-darwin-64': 1.20260317.1 + '@cloudflare/workerd-darwin-arm64': 1.20260317.1 + '@cloudflare/workerd-linux-64': 1.20260317.1 + '@cloudflare/workerd-linux-arm64': 1.20260317.1 + '@cloudflare/workerd-windows-64': 1.20260317.1 - wrangler@4.70.0: + wrangler@4.75.0: dependencies: '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1) + '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260301.1 + miniflare: 4.20260317.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 + workerd: 1.20260317.1 optionalDependencies: fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - utf-8-validate + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.18.0: {} yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3